diff --git a/README.md b/README.md index 35acda5..550047c 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,14 @@ Annotate keyboard key presses on a video timeline and preview/export mechanical switch sounds mixed with the video audio. +Keyboard sounds are sourced from [kbsim](https://github.com/tplai/kbsim) (MIT license) — recorded press and release samples with per-key variation. + ## Requirements - [uv](https://docs.astral.sh/uv/) (Python package manager) - Node.js 18+ and npm - **ffmpeg** on your PATH (required by pydub for audio export) +- **git** (to fetch kbsim sample assets) ```bash # macOS @@ -21,6 +24,9 @@ uv sync # Frontend dependencies cd frontend && npm install + +# Download kbsim switch samples (~151 MP3 files) +./scripts/fetch_kbsim_samples.sh ``` ## Development @@ -50,8 +56,10 @@ Open http://127.0.0.1:8000 1. **Open Video** — load an MP4 file 2. Press keys while the playhead is at the desired time to add markers (works while playing or paused) + - Press sound plays on keydown; release sound plays on keyup + - Each physical key maps to a stable press variant (same key always sounds the same) 3. Select markers to override keys, nudge with arrow keys, or multi-select with marquee drag -4. Choose a mechanical switch sound from the dropdown +4. Choose a mechanical switch sound from the dropdown (13 kbsim profiles) 5. **Save Project** / **Open Project** — JSON with version, markers, and switch setting (no video path) 6. **Export Audio** — download a WAV file of keyboard sounds only, full video length @@ -69,19 +77,31 @@ Open http://127.0.0.1:8000 ```json { - "version": 1, - "switch": "cherry-mx-blue", + "version": 2, + "switch": "mxbrown", "markers": [ - { "id": "m1", "time": 1.234, "key": "a" } + { "id": "m1", "time": 1.234, "key": "a", "code": "KeyA", "releaseTime": 1.312 } ] } ``` +- `time` — press timestamp (seconds) +- `releaseTime` — keyup timestamp; omitted in v1 projects default to press + 80ms at playback/export +- `code` — `event.code` for stable per-key sound mapping (e.g. `KeyA`, `Digit1`) + +v1 projects with `cherry-mx-blue` / `cherry-mx-red` / `cherry-mx-brown` switches are migrated automatically. + Re-open the video manually after loading a project file. ## Switch samples -Bundled samples are short synthetic click sounds representing Cherry MX Blue (clicky), Red (linear), and Brown (tactile) switches. +Samples live in `assets/samples/kbsim/` and are fetched via `scripts/fetch_kbsim_samples.sh`. Available profiles: + +NovelKeys Creams, Holy Pandas, Alpacas, Turquoise Tealios, Gateron Black Inks, Gateron Red Inks, Cherry MX Blacks, Cherry MX Browns, Cherry MX Blues, Kailh Box Navies, Buckling Spring, SKCM Blue Alps, Topre. + +## Attribution + +Keyboard sound samples from [tplai/kbsim](https://github.com/tplai/kbsim) by Thomas Lai (MIT license). ## Known limitations diff --git a/assets/samples/cherry-mx-blue.wav b/assets/samples/cherry-mx-blue.wav deleted file mode 100644 index 39f0e8a..0000000 Binary files a/assets/samples/cherry-mx-blue.wav and /dev/null differ diff --git a/assets/samples/cherry-mx-brown.wav b/assets/samples/cherry-mx-brown.wav deleted file mode 100644 index 9a8ac09..0000000 Binary files a/assets/samples/cherry-mx-brown.wav and /dev/null differ diff --git a/assets/samples/cherry-mx-red.wav b/assets/samples/cherry-mx-red.wav deleted file mode 100644 index 12b0f1e..0000000 Binary files a/assets/samples/cherry-mx-red.wav and /dev/null differ diff --git a/assets/samples/kbsim/alpaca/press/BACKSPACE.mp3 b/assets/samples/kbsim/alpaca/press/BACKSPACE.mp3 new file mode 100644 index 0000000..1e90e07 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/ENTER.mp3 b/assets/samples/kbsim/alpaca/press/ENTER.mp3 new file mode 100644 index 0000000..fc65e06 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/GENERIC_R0.mp3 b/assets/samples/kbsim/alpaca/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..5add954 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/GENERIC_R1.mp3 b/assets/samples/kbsim/alpaca/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..057f144 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/GENERIC_R2.mp3 b/assets/samples/kbsim/alpaca/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..e51670b Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/GENERIC_R3.mp3 b/assets/samples/kbsim/alpaca/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..b278723 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/GENERIC_R4.mp3 b/assets/samples/kbsim/alpaca/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..619e6f9 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/alpaca/press/SPACE.mp3 b/assets/samples/kbsim/alpaca/press/SPACE.mp3 new file mode 100644 index 0000000..5351e12 Binary files /dev/null and b/assets/samples/kbsim/alpaca/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/alpaca/release/BACKSPACE.mp3 b/assets/samples/kbsim/alpaca/release/BACKSPACE.mp3 new file mode 100644 index 0000000..969e3f3 Binary files /dev/null and b/assets/samples/kbsim/alpaca/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/alpaca/release/ENTER.mp3 b/assets/samples/kbsim/alpaca/release/ENTER.mp3 new file mode 100644 index 0000000..08d112d Binary files /dev/null and b/assets/samples/kbsim/alpaca/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/alpaca/release/GENERIC.mp3 b/assets/samples/kbsim/alpaca/release/GENERIC.mp3 new file mode 100644 index 0000000..22482a4 Binary files /dev/null and b/assets/samples/kbsim/alpaca/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/alpaca/release/SPACE.mp3 b/assets/samples/kbsim/alpaca/release/SPACE.mp3 new file mode 100644 index 0000000..92d57bf Binary files /dev/null and b/assets/samples/kbsim/alpaca/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/BACKSPACE.mp3 b/assets/samples/kbsim/alpsblue/press/BACKSPACE.mp3 new file mode 100644 index 0000000..9ab2a34 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/ENTER.mp3 b/assets/samples/kbsim/alpsblue/press/ENTER.mp3 new file mode 100644 index 0000000..8432b62 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/GENERIC_R0.mp3 b/assets/samples/kbsim/alpsblue/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..2d4ad90 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/GENERIC_R1.mp3 b/assets/samples/kbsim/alpsblue/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..9d2a86e Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/GENERIC_R2.mp3 b/assets/samples/kbsim/alpsblue/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..cb77b3c Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/GENERIC_R3.mp3 b/assets/samples/kbsim/alpsblue/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..d09cd21 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/GENERIC_R4.mp3 b/assets/samples/kbsim/alpsblue/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..21d7f6a Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/press/SPACE.mp3 b/assets/samples/kbsim/alpsblue/press/SPACE.mp3 new file mode 100644 index 0000000..4ef43e2 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/release/BACKSPACE.mp3 b/assets/samples/kbsim/alpsblue/release/BACKSPACE.mp3 new file mode 100644 index 0000000..fea1ae4 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/release/ENTER.mp3 b/assets/samples/kbsim/alpsblue/release/ENTER.mp3 new file mode 100644 index 0000000..2012d57 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/release/GENERIC.mp3 b/assets/samples/kbsim/alpsblue/release/GENERIC.mp3 new file mode 100644 index 0000000..a85adb3 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/release/GENERIC_long.mp3 b/assets/samples/kbsim/alpsblue/release/GENERIC_long.mp3 new file mode 100644 index 0000000..5e98e48 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/release/GENERIC_long.mp3 differ diff --git a/assets/samples/kbsim/alpsblue/release/SPACE.mp3 b/assets/samples/kbsim/alpsblue/release/SPACE.mp3 new file mode 100644 index 0000000..6fca397 Binary files /dev/null and b/assets/samples/kbsim/alpsblue/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/BACKSPACE.mp3 b/assets/samples/kbsim/boxnavy/press/BACKSPACE.mp3 new file mode 100644 index 0000000..b8d2584 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/ENTER.mp3 b/assets/samples/kbsim/boxnavy/press/ENTER.mp3 new file mode 100644 index 0000000..b2eccd7 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/GENERIC_R0.mp3 b/assets/samples/kbsim/boxnavy/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..317e40d Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/GENERIC_R1.mp3 b/assets/samples/kbsim/boxnavy/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..4f6af1c Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/GENERIC_R2.mp3 b/assets/samples/kbsim/boxnavy/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..6dcac82 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/GENERIC_R3.mp3 b/assets/samples/kbsim/boxnavy/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..412c082 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/GENERIC_R4.mp3 b/assets/samples/kbsim/boxnavy/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..8c7d986 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/press/SPACE.mp3 b/assets/samples/kbsim/boxnavy/press/SPACE.mp3 new file mode 100644 index 0000000..866c52b Binary files /dev/null and b/assets/samples/kbsim/boxnavy/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/release/BACKSPACE.mp3 b/assets/samples/kbsim/boxnavy/release/BACKSPACE.mp3 new file mode 100644 index 0000000..0387acb Binary files /dev/null and b/assets/samples/kbsim/boxnavy/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/release/ENTER.mp3 b/assets/samples/kbsim/boxnavy/release/ENTER.mp3 new file mode 100644 index 0000000..404b5d7 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/release/GENERIC.mp3 b/assets/samples/kbsim/boxnavy/release/GENERIC.mp3 new file mode 100644 index 0000000..26b7c0e Binary files /dev/null and b/assets/samples/kbsim/boxnavy/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/boxnavy/release/SPACE.mp3 b/assets/samples/kbsim/boxnavy/release/SPACE.mp3 new file mode 100644 index 0000000..ae49d94 Binary files /dev/null and b/assets/samples/kbsim/boxnavy/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/BACKSPACE.mp3 b/assets/samples/kbsim/buckling/press/BACKSPACE.mp3 new file mode 100644 index 0000000..a40f95f Binary files /dev/null and b/assets/samples/kbsim/buckling/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/ENTER.mp3 b/assets/samples/kbsim/buckling/press/ENTER.mp3 new file mode 100644 index 0000000..18391bc Binary files /dev/null and b/assets/samples/kbsim/buckling/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/GENERIC_R0.mp3 b/assets/samples/kbsim/buckling/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..2c10024 Binary files /dev/null and b/assets/samples/kbsim/buckling/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/GENERIC_R1.mp3 b/assets/samples/kbsim/buckling/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..f161874 Binary files /dev/null and b/assets/samples/kbsim/buckling/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/GENERIC_R2.mp3 b/assets/samples/kbsim/buckling/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..acf7b51 Binary files /dev/null and b/assets/samples/kbsim/buckling/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/GENERIC_R3.mp3 b/assets/samples/kbsim/buckling/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..20ee372 Binary files /dev/null and b/assets/samples/kbsim/buckling/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/GENERIC_R4.mp3 b/assets/samples/kbsim/buckling/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..24e9c69 Binary files /dev/null and b/assets/samples/kbsim/buckling/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/buckling/press/SPACE.mp3 b/assets/samples/kbsim/buckling/press/SPACE.mp3 new file mode 100644 index 0000000..426d5e1 Binary files /dev/null and b/assets/samples/kbsim/buckling/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/buckling/release/BACKSPACE.mp3 b/assets/samples/kbsim/buckling/release/BACKSPACE.mp3 new file mode 100644 index 0000000..ccddfbc Binary files /dev/null and b/assets/samples/kbsim/buckling/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/buckling/release/ENTER.mp3 b/assets/samples/kbsim/buckling/release/ENTER.mp3 new file mode 100644 index 0000000..4161eb5 Binary files /dev/null and b/assets/samples/kbsim/buckling/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/buckling/release/GENERIC.mp3 b/assets/samples/kbsim/buckling/release/GENERIC.mp3 new file mode 100644 index 0000000..6a80d56 Binary files /dev/null and b/assets/samples/kbsim/buckling/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/buckling/release/SPACE.mp3 b/assets/samples/kbsim/buckling/release/SPACE.mp3 new file mode 100644 index 0000000..661b956 Binary files /dev/null and b/assets/samples/kbsim/buckling/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/cream/press/BACKSPACE.mp3 b/assets/samples/kbsim/cream/press/BACKSPACE.mp3 new file mode 100644 index 0000000..1a78cab Binary files /dev/null and b/assets/samples/kbsim/cream/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/cream/press/ENTER.mp3 b/assets/samples/kbsim/cream/press/ENTER.mp3 new file mode 100644 index 0000000..e49e835 Binary files /dev/null and b/assets/samples/kbsim/cream/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/cream/press/GENERIC_R0.mp3 b/assets/samples/kbsim/cream/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..62c7d84 Binary files /dev/null and b/assets/samples/kbsim/cream/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/cream/press/GENERIC_R1.mp3 b/assets/samples/kbsim/cream/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..2ccc7f8 Binary files /dev/null and b/assets/samples/kbsim/cream/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/cream/press/GENERIC_R2.mp3 b/assets/samples/kbsim/cream/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..f24ba50 Binary files /dev/null and b/assets/samples/kbsim/cream/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/cream/press/GENERIC_R3.mp3 b/assets/samples/kbsim/cream/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..6e647c5 Binary files /dev/null and b/assets/samples/kbsim/cream/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/cream/press/GENERIC_R4.mp3 b/assets/samples/kbsim/cream/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..b3c9544 Binary files /dev/null and b/assets/samples/kbsim/cream/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/cream/press/SPACE.mp3 b/assets/samples/kbsim/cream/press/SPACE.mp3 new file mode 100644 index 0000000..fb7fb8e Binary files /dev/null and b/assets/samples/kbsim/cream/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/cream/release/BACKSPACE.mp3 b/assets/samples/kbsim/cream/release/BACKSPACE.mp3 new file mode 100644 index 0000000..3ad32b9 Binary files /dev/null and b/assets/samples/kbsim/cream/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/cream/release/ENTER.mp3 b/assets/samples/kbsim/cream/release/ENTER.mp3 new file mode 100644 index 0000000..defbcbb Binary files /dev/null and b/assets/samples/kbsim/cream/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/cream/release/GENERIC.mp3 b/assets/samples/kbsim/cream/release/GENERIC.mp3 new file mode 100644 index 0000000..4da558a Binary files /dev/null and b/assets/samples/kbsim/cream/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/cream/release/SPACE.mp3 b/assets/samples/kbsim/cream/release/SPACE.mp3 new file mode 100644 index 0000000..777c5ac Binary files /dev/null and b/assets/samples/kbsim/cream/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/BACKSPACE.mp3 b/assets/samples/kbsim/holypanda/press/BACKSPACE.mp3 new file mode 100644 index 0000000..4bfa1c7 Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/ENTER.mp3 b/assets/samples/kbsim/holypanda/press/ENTER.mp3 new file mode 100644 index 0000000..ee1d781 Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/GENERIC_R0.mp3 b/assets/samples/kbsim/holypanda/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..031b89b Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/GENERIC_R1.mp3 b/assets/samples/kbsim/holypanda/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..2a23c63 Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/GENERIC_R2.mp3 b/assets/samples/kbsim/holypanda/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..b31d2fb Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/GENERIC_R3.mp3 b/assets/samples/kbsim/holypanda/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..a836791 Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/GENERIC_R4.mp3 b/assets/samples/kbsim/holypanda/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..96297b3 Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/holypanda/press/SPACE.mp3 b/assets/samples/kbsim/holypanda/press/SPACE.mp3 new file mode 100644 index 0000000..326dcca Binary files /dev/null and b/assets/samples/kbsim/holypanda/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/holypanda/release/BACKSPACE.mp3 b/assets/samples/kbsim/holypanda/release/BACKSPACE.mp3 new file mode 100644 index 0000000..f4df8a0 Binary files /dev/null and b/assets/samples/kbsim/holypanda/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/holypanda/release/ENTER.mp3 b/assets/samples/kbsim/holypanda/release/ENTER.mp3 new file mode 100644 index 0000000..ca6618a Binary files /dev/null and b/assets/samples/kbsim/holypanda/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/holypanda/release/GENERIC.mp3 b/assets/samples/kbsim/holypanda/release/GENERIC.mp3 new file mode 100644 index 0000000..3897e0d Binary files /dev/null and b/assets/samples/kbsim/holypanda/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/holypanda/release/SPACE.mp3 b/assets/samples/kbsim/holypanda/release/SPACE.mp3 new file mode 100644 index 0000000..556c2a0 Binary files /dev/null and b/assets/samples/kbsim/holypanda/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/BACKSPACE.mp3 b/assets/samples/kbsim/inkblack/press/BACKSPACE.mp3 new file mode 100644 index 0000000..956ad6c Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/ENTER.mp3 b/assets/samples/kbsim/inkblack/press/ENTER.mp3 new file mode 100644 index 0000000..bda40d9 Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/GENERIC_R0.mp3 b/assets/samples/kbsim/inkblack/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..853a9e9 Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/GENERIC_R1.mp3 b/assets/samples/kbsim/inkblack/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..198190b Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/GENERIC_R2.mp3 b/assets/samples/kbsim/inkblack/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..a8add3c Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/GENERIC_R3.mp3 b/assets/samples/kbsim/inkblack/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..a8add3c Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/GENERIC_R4.mp3 b/assets/samples/kbsim/inkblack/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..3a3f507 Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/inkblack/press/SPACE.mp3 b/assets/samples/kbsim/inkblack/press/SPACE.mp3 new file mode 100644 index 0000000..67f2694 Binary files /dev/null and b/assets/samples/kbsim/inkblack/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/inkblack/release/BACKSPACE.mp3 b/assets/samples/kbsim/inkblack/release/BACKSPACE.mp3 new file mode 100644 index 0000000..07755fa Binary files /dev/null and b/assets/samples/kbsim/inkblack/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/inkblack/release/ENTER.mp3 b/assets/samples/kbsim/inkblack/release/ENTER.mp3 new file mode 100644 index 0000000..e2a26c1 Binary files /dev/null and b/assets/samples/kbsim/inkblack/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/inkblack/release/GENERIC.mp3 b/assets/samples/kbsim/inkblack/release/GENERIC.mp3 new file mode 100644 index 0000000..663cb46 Binary files /dev/null and b/assets/samples/kbsim/inkblack/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/inkblack/release/SPACE.mp3 b/assets/samples/kbsim/inkblack/release/SPACE.mp3 new file mode 100644 index 0000000..b4e34dd Binary files /dev/null and b/assets/samples/kbsim/inkblack/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/BACKSPACE.mp3 b/assets/samples/kbsim/inkred/press/BACKSPACE.mp3 new file mode 100644 index 0000000..c7e9abe Binary files /dev/null and b/assets/samples/kbsim/inkred/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/ENTER.mp3 b/assets/samples/kbsim/inkred/press/ENTER.mp3 new file mode 100644 index 0000000..c5df7f2 Binary files /dev/null and b/assets/samples/kbsim/inkred/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/GENERIC_R0.mp3 b/assets/samples/kbsim/inkred/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..9b2e920 Binary files /dev/null and b/assets/samples/kbsim/inkred/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/GENERIC_R1.mp3 b/assets/samples/kbsim/inkred/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..d67a2c7 Binary files /dev/null and b/assets/samples/kbsim/inkred/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/GENERIC_R2.mp3 b/assets/samples/kbsim/inkred/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..81673d0 Binary files /dev/null and b/assets/samples/kbsim/inkred/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/GENERIC_R3.mp3 b/assets/samples/kbsim/inkred/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..db4f0b6 Binary files /dev/null and b/assets/samples/kbsim/inkred/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/GENERIC_R4.mp3 b/assets/samples/kbsim/inkred/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..4fad45f Binary files /dev/null and b/assets/samples/kbsim/inkred/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/inkred/press/SPACE.mp3 b/assets/samples/kbsim/inkred/press/SPACE.mp3 new file mode 100644 index 0000000..3283fd6 Binary files /dev/null and b/assets/samples/kbsim/inkred/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/inkred/release/BACKSPACE.mp3 b/assets/samples/kbsim/inkred/release/BACKSPACE.mp3 new file mode 100644 index 0000000..9a25df4 Binary files /dev/null and b/assets/samples/kbsim/inkred/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/inkred/release/ENTER.mp3 b/assets/samples/kbsim/inkred/release/ENTER.mp3 new file mode 100644 index 0000000..2a4baba Binary files /dev/null and b/assets/samples/kbsim/inkred/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/inkred/release/GENERIC.mp3 b/assets/samples/kbsim/inkred/release/GENERIC.mp3 new file mode 100644 index 0000000..c802221 Binary files /dev/null and b/assets/samples/kbsim/inkred/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/inkred/release/SPACE.mp3 b/assets/samples/kbsim/inkred/release/SPACE.mp3 new file mode 100644 index 0000000..c167c19 Binary files /dev/null and b/assets/samples/kbsim/inkred/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/manifest.json b/assets/samples/kbsim/manifest.json new file mode 100644 index 0000000..492109a --- /dev/null +++ b/assets/samples/kbsim/manifest.json @@ -0,0 +1,231 @@ +{ + "alpaca": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "alpsblue": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "GENERIC_long", + "SPACE" + ] + }, + "boxnavy": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "buckling": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "cream": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "holypanda": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "inkblack": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "inkred": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "mxblack": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "mxblue": { + "press": [ + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4" + ], + "release": [ + "GENERIC" + ] + }, + "mxbrown": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "topre": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + }, + "turquoise": { + "press": [ + "BACKSPACE", + "ENTER", + "GENERIC_R0", + "GENERIC_R1", + "GENERIC_R2", + "GENERIC_R3", + "GENERIC_R4", + "SPACE" + ], + "release": [ + "BACKSPACE", + "ENTER", + "GENERIC", + "SPACE" + ] + } +} diff --git a/assets/samples/kbsim/mxblack/press/BACKSPACE.mp3 b/assets/samples/kbsim/mxblack/press/BACKSPACE.mp3 new file mode 100644 index 0000000..23258bb Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/ENTER.mp3 b/assets/samples/kbsim/mxblack/press/ENTER.mp3 new file mode 100644 index 0000000..9956074 Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/GENERIC_R0.mp3 b/assets/samples/kbsim/mxblack/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..8a47132 Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/GENERIC_R1.mp3 b/assets/samples/kbsim/mxblack/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..5d8a7ce Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/GENERIC_R2.mp3 b/assets/samples/kbsim/mxblack/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..7e4e373 Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/GENERIC_R3.mp3 b/assets/samples/kbsim/mxblack/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..1d2de16 Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/GENERIC_R4.mp3 b/assets/samples/kbsim/mxblack/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..f8851a8 Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/mxblack/press/SPACE.mp3 b/assets/samples/kbsim/mxblack/press/SPACE.mp3 new file mode 100644 index 0000000..32c6751 Binary files /dev/null and b/assets/samples/kbsim/mxblack/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/mxblack/release/BACKSPACE.mp3 b/assets/samples/kbsim/mxblack/release/BACKSPACE.mp3 new file mode 100644 index 0000000..02e0d0e Binary files /dev/null and b/assets/samples/kbsim/mxblack/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/mxblack/release/ENTER.mp3 b/assets/samples/kbsim/mxblack/release/ENTER.mp3 new file mode 100644 index 0000000..576e0d9 Binary files /dev/null and b/assets/samples/kbsim/mxblack/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/mxblack/release/GENERIC.mp3 b/assets/samples/kbsim/mxblack/release/GENERIC.mp3 new file mode 100644 index 0000000..a3c6404 Binary files /dev/null and b/assets/samples/kbsim/mxblack/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/mxblack/release/SPACE.mp3 b/assets/samples/kbsim/mxblack/release/SPACE.mp3 new file mode 100644 index 0000000..1ecc139 Binary files /dev/null and b/assets/samples/kbsim/mxblack/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/mxblue/press/GENERIC_R0.mp3 b/assets/samples/kbsim/mxblue/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..c9bc9a0 Binary files /dev/null and b/assets/samples/kbsim/mxblue/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/mxblue/press/GENERIC_R1.mp3 b/assets/samples/kbsim/mxblue/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..9de4744 Binary files /dev/null and b/assets/samples/kbsim/mxblue/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/mxblue/press/GENERIC_R2.mp3 b/assets/samples/kbsim/mxblue/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..81b1b62 Binary files /dev/null and b/assets/samples/kbsim/mxblue/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/mxblue/press/GENERIC_R3.mp3 b/assets/samples/kbsim/mxblue/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..c47362b Binary files /dev/null and b/assets/samples/kbsim/mxblue/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/mxblue/press/GENERIC_R4.mp3 b/assets/samples/kbsim/mxblue/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..2d41a4d Binary files /dev/null and b/assets/samples/kbsim/mxblue/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/mxblue/release/GENERIC.mp3 b/assets/samples/kbsim/mxblue/release/GENERIC.mp3 new file mode 100644 index 0000000..edd0abb Binary files /dev/null and b/assets/samples/kbsim/mxblue/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/BACKSPACE.mp3 b/assets/samples/kbsim/mxbrown/press/BACKSPACE.mp3 new file mode 100644 index 0000000..0bae8fa Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/ENTER.mp3 b/assets/samples/kbsim/mxbrown/press/ENTER.mp3 new file mode 100644 index 0000000..6e4f758 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/GENERIC_R0.mp3 b/assets/samples/kbsim/mxbrown/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..d525d10 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/GENERIC_R1.mp3 b/assets/samples/kbsim/mxbrown/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..4634843 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/GENERIC_R2.mp3 b/assets/samples/kbsim/mxbrown/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..6205266 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/GENERIC_R3.mp3 b/assets/samples/kbsim/mxbrown/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..3519815 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/GENERIC_R4.mp3 b/assets/samples/kbsim/mxbrown/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..94ffaea Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/press/SPACE.mp3 b/assets/samples/kbsim/mxbrown/press/SPACE.mp3 new file mode 100644 index 0000000..0dabcb0 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/release/BACKSPACE.mp3 b/assets/samples/kbsim/mxbrown/release/BACKSPACE.mp3 new file mode 100644 index 0000000..0bc7d05 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/release/ENTER.mp3 b/assets/samples/kbsim/mxbrown/release/ENTER.mp3 new file mode 100644 index 0000000..9d24928 Binary files /dev/null and b/assets/samples/kbsim/mxbrown/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/release/GENERIC.mp3 b/assets/samples/kbsim/mxbrown/release/GENERIC.mp3 new file mode 100644 index 0000000..4f8114f Binary files /dev/null and b/assets/samples/kbsim/mxbrown/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/mxbrown/release/SPACE.mp3 b/assets/samples/kbsim/mxbrown/release/SPACE.mp3 new file mode 100644 index 0000000..d1a8a8e Binary files /dev/null and b/assets/samples/kbsim/mxbrown/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/topre/press/BACKSPACE.mp3 b/assets/samples/kbsim/topre/press/BACKSPACE.mp3 new file mode 100644 index 0000000..710ab53 Binary files /dev/null and b/assets/samples/kbsim/topre/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/topre/press/ENTER.mp3 b/assets/samples/kbsim/topre/press/ENTER.mp3 new file mode 100644 index 0000000..ca5cfaf Binary files /dev/null and b/assets/samples/kbsim/topre/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/topre/press/GENERIC_R0.mp3 b/assets/samples/kbsim/topre/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..00990d3 Binary files /dev/null and b/assets/samples/kbsim/topre/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/topre/press/GENERIC_R1.mp3 b/assets/samples/kbsim/topre/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..a3acbe6 Binary files /dev/null and b/assets/samples/kbsim/topre/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/topre/press/GENERIC_R2.mp3 b/assets/samples/kbsim/topre/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..7983036 Binary files /dev/null and b/assets/samples/kbsim/topre/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/topre/press/GENERIC_R3.mp3 b/assets/samples/kbsim/topre/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..3f56111 Binary files /dev/null and b/assets/samples/kbsim/topre/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/topre/press/GENERIC_R4.mp3 b/assets/samples/kbsim/topre/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..723e752 Binary files /dev/null and b/assets/samples/kbsim/topre/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/topre/press/SPACE.mp3 b/assets/samples/kbsim/topre/press/SPACE.mp3 new file mode 100644 index 0000000..9f5a96e Binary files /dev/null and b/assets/samples/kbsim/topre/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/topre/release/BACKSPACE.mp3 b/assets/samples/kbsim/topre/release/BACKSPACE.mp3 new file mode 100644 index 0000000..3c4ba2b Binary files /dev/null and b/assets/samples/kbsim/topre/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/topre/release/ENTER.mp3 b/assets/samples/kbsim/topre/release/ENTER.mp3 new file mode 100644 index 0000000..2108d88 Binary files /dev/null and b/assets/samples/kbsim/topre/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/topre/release/GENERIC.mp3 b/assets/samples/kbsim/topre/release/GENERIC.mp3 new file mode 100644 index 0000000..536b7a0 Binary files /dev/null and b/assets/samples/kbsim/topre/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/topre/release/SPACE.mp3 b/assets/samples/kbsim/topre/release/SPACE.mp3 new file mode 100644 index 0000000..62a01a6 Binary files /dev/null and b/assets/samples/kbsim/topre/release/SPACE.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/BACKSPACE.mp3 b/assets/samples/kbsim/turquoise/press/BACKSPACE.mp3 new file mode 100644 index 0000000..69c6029 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/ENTER.mp3 b/assets/samples/kbsim/turquoise/press/ENTER.mp3 new file mode 100644 index 0000000..6299435 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/ENTER.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/GENERIC_R0.mp3 b/assets/samples/kbsim/turquoise/press/GENERIC_R0.mp3 new file mode 100644 index 0000000..9c4d891 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/GENERIC_R0.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/GENERIC_R1.mp3 b/assets/samples/kbsim/turquoise/press/GENERIC_R1.mp3 new file mode 100644 index 0000000..2ce90c3 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/GENERIC_R1.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/GENERIC_R2.mp3 b/assets/samples/kbsim/turquoise/press/GENERIC_R2.mp3 new file mode 100644 index 0000000..37ccdb4 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/GENERIC_R2.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/GENERIC_R3.mp3 b/assets/samples/kbsim/turquoise/press/GENERIC_R3.mp3 new file mode 100644 index 0000000..a41d3a1 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/GENERIC_R3.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/GENERIC_R4.mp3 b/assets/samples/kbsim/turquoise/press/GENERIC_R4.mp3 new file mode 100644 index 0000000..af772ff Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/GENERIC_R4.mp3 differ diff --git a/assets/samples/kbsim/turquoise/press/SPACE.mp3 b/assets/samples/kbsim/turquoise/press/SPACE.mp3 new file mode 100644 index 0000000..4cd97b5 Binary files /dev/null and b/assets/samples/kbsim/turquoise/press/SPACE.mp3 differ diff --git a/assets/samples/kbsim/turquoise/release/BACKSPACE.mp3 b/assets/samples/kbsim/turquoise/release/BACKSPACE.mp3 new file mode 100644 index 0000000..e05b35f Binary files /dev/null and b/assets/samples/kbsim/turquoise/release/BACKSPACE.mp3 differ diff --git a/assets/samples/kbsim/turquoise/release/ENTER.mp3 b/assets/samples/kbsim/turquoise/release/ENTER.mp3 new file mode 100644 index 0000000..105035e Binary files /dev/null and b/assets/samples/kbsim/turquoise/release/ENTER.mp3 differ diff --git a/assets/samples/kbsim/turquoise/release/GENERIC.mp3 b/assets/samples/kbsim/turquoise/release/GENERIC.mp3 new file mode 100644 index 0000000..7df554b Binary files /dev/null and b/assets/samples/kbsim/turquoise/release/GENERIC.mp3 differ diff --git a/assets/samples/kbsim/turquoise/release/SPACE.mp3 b/assets/samples/kbsim/turquoise/release/SPACE.mp3 new file mode 100644 index 0000000..10c5b6c Binary files /dev/null and b/assets/samples/kbsim/turquoise/release/SPACE.mp3 differ diff --git a/backend/audio_export.py b/backend/audio_export.py index 94210b5..4b4c643 100644 --- a/backend/audio_export.py +++ b/backend/audio_export.py @@ -3,20 +3,18 @@ from pathlib import Path from pydub import AudioSegment -from backend.models import ExportRequest, SwitchType +from backend.key_sound_map import ( + MANIFEST, + marker_release_time, + resolve_marker_samples, + sample_path, +) +from backend.models import ExportRequest SAMPLE_RATE = 48000 -SAMPLES_DIR = Path(__file__).resolve().parent.parent / "assets" / "samples" - -SWITCH_FILES: dict[SwitchType, str] = { - "cherry-mx-blue": "cherry-mx-blue.wav", - "cherry-mx-red": "cherry-mx-red.wav", - "cherry-mx-brown": "cherry-mx-brown.wav", -} -def _load_sample(switch: SwitchType) -> AudioSegment: - path = SAMPLES_DIR / SWITCH_FILES[switch] +def _load_sample(path: Path) -> AudioSegment: if not path.exists(): raise FileNotFoundError(f"Sample not found: {path}") sample = AudioSegment.from_file(path) @@ -24,14 +22,29 @@ def _load_sample(switch: SwitchType) -> AudioSegment: def export_keyboard_audio(request: ExportRequest) -> bytes: + if request.switch not in MANIFEST: + raise FileNotFoundError(f"Unknown switch: {request.switch}") + duration_ms = int(request.duration * 1000) base = AudioSegment.silent(duration=duration_ms, frame_rate=SAMPLE_RATE) - click = _load_sample(request.switch) for marker in sorted(request.markers, key=lambda m: m.time): - position_ms = int(marker.time * 1000) - if position_ms < duration_ms: - base = base.overlay(click, position=position_ms) + press_key, release_key = resolve_marker_samples( + request.switch, + marker.key, + marker.code, + ) + press_sample = _load_sample(sample_path(request.switch, "press", press_key)) + release_sample = _load_sample(sample_path(request.switch, "release", release_key)) + + press_ms = int(marker.time * 1000) + if press_ms < duration_ms: + base = base.overlay(press_sample, position=press_ms) + + release_time = marker_release_time(marker.time, marker.release_time) + release_ms = int(release_time * 1000) + if release_ms < duration_ms: + base = base.overlay(release_sample, position=release_ms) buffer = io.BytesIO() base.export(buffer, format="wav") diff --git a/backend/key_sound_map.py b/backend/key_sound_map.py new file mode 100644 index 0000000..5bb8741 --- /dev/null +++ b/backend/key_sound_map.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Optional + +SAMPLES_DIR = Path(__file__).resolve().parent.parent / "assets" / "samples" / "kbsim" +MANIFEST_PATH = SAMPLES_DIR / "manifest.json" +DEFAULT_RELEASE_OFFSET = 0.08 + +SPECIAL_KEY_SAMPLES: dict[str, str] = { + "Space": "SPACE", + "Enter": "ENTER", + "Backspace": "BACKSPACE", +} + + +def _load_manifest() -> dict[str, dict[str, list[str]]]: + with MANIFEST_PATH.open(encoding="utf-8") as f: + return json.load(f) + + +MANIFEST = _load_manifest() + + +def hash_code(value: str) -> int: + """djb2 hash — mirrored in frontend/src/lib/audio/keySoundMap.ts""" + hash_val = 5381 + for ch in value: + hash_val = ((hash_val * 33) ^ ord(ch)) & 0xFFFFFFFF + return hash_val + + +def press_variant_index(code: str) -> int: + return hash_code(code) % 5 + + +def press_variant_key(code: str) -> str: + return f"GENERIC_R{press_variant_index(code)}" + + +def resolve_press_sample_key(key_label: str, code: str, available: list[str]) -> str: + special = SPECIAL_KEY_SAMPLES.get(key_label) + if special and special in available: + return special + variant = press_variant_key(code) + if variant in available: + return variant + for sample in available: + if sample.startswith("GENERIC_R"): + return sample + return available[0] + + +def resolve_release_sample_key(key_label: str, available: list[str]) -> str: + special = SPECIAL_KEY_SAMPLES.get(key_label) + if special and special in available: + return special + if "GENERIC" in available: + return "GENERIC" + return available[0] + + +def sample_path(switch_id: str, phase: str, sample_key: str) -> Path: + return SAMPLES_DIR / switch_id / phase / f"{sample_key}.mp3" + + +def marker_release_time(time: float, release_time: Optional[float]) -> float: + return release_time if release_time is not None else time + DEFAULT_RELEASE_OFFSET + + +def resolve_marker_samples( + switch_id: str, + key_label: str, + code: Optional[str], +) -> tuple[str, str]: + switch_manifest = MANIFEST[switch_id] + press_available = switch_manifest["press"] + release_available = switch_manifest["release"] + resolved_code = code or key_label + press_key = resolve_press_sample_key(key_label, resolved_code, press_available) + release_key = resolve_release_sample_key(key_label, release_available) + return press_key, release_key diff --git a/backend/models.py b/backend/models.py index 2fde559..8a50c13 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,23 +1,43 @@ -from typing import Literal +from __future__ import annotations + +from typing import Literal, Optional from pydantic import BaseModel, Field -SwitchType = Literal["cherry-mx-blue", "cherry-mx-red", "cherry-mx-brown"] +SwitchType = Literal[ + "cream", + "holypanda", + "alpaca", + "turquoise", + "inkblack", + "inkred", + "mxblack", + "mxbrown", + "mxblue", + "boxnavy", + "buckling", + "alpsblue", + "topre", +] class Marker(BaseModel): id: str time: float = Field(ge=0) key: str + code: Optional[str] = None + release_time: Optional[float] = Field(default=None, ge=0, alias="releaseTime") + + model_config = {"populate_by_name": True} class Project(BaseModel): - version: int = 1 - switch: SwitchType = "cherry-mx-blue" + version: int = 2 + switch: SwitchType = "mxbrown" markers: list[Marker] = Field(default_factory=list) class ExportRequest(BaseModel): duration: float = Field(gt=0) - switch: SwitchType = "cherry-mx-blue" + switch: SwitchType = "mxbrown" markers: list[Marker] = Field(default_factory=list) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 99fcefd..225804a 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -3,7 +3,9 @@ import { createAudioEngine } from './lib/audio/engine'; import { isEditableTarget, + keyCode, keyLabel, + shouldDeleteSelected, shouldOverrideSelected, shouldRecordMarker, } from './lib/keyboard'; @@ -32,6 +34,7 @@ let audioEngine = createAudioEngine(); let statusMessage = $state(''); let lastScheduledTime = -1; + const pendingPresses = new Map(); function rescheduleAudio() { if (!videoEl) return; @@ -79,12 +82,10 @@ return; } - if (event.key === 'Backspace' || event.key === 'Delete') { - if (app.selectedIds.size > 0) { - event.preventDefault(); - deleteMarkers(new Set(app.selectedIds)); - rescheduleAudio(); - } + if (shouldDeleteSelected(event, app.selectedIds.size > 0)) { + event.preventDefault(); + deleteMarkers(new Set(app.selectedIds)); + rescheduleAudio(); return; } @@ -109,14 +110,36 @@ const label = keyLabel(event); if (!label) return; event.preventDefault(); + const code = keyCode(event); const time = snapToFrame(videoEl.currentTime); - addMarker(time, label); + const marker = addMarker(time, label, code); + pendingPresses.set(code, marker.id); + audioEngine.playKey(code, label, 'press'); rescheduleAudio(); } }; + const onKeyUp = (event: KeyboardEvent) => { + if (isEditableTarget(event.target)) return; + const code = keyCode(event); + const markerId = pendingPresses.get(code); + if (!markerId || !videoEl) return; + + event.preventDefault(); + const label = keyLabel(event); + pendingPresses.delete(code); + const releaseTime = snapToFrame(videoEl.currentTime); + updateMarker(markerId, { releaseTime }); + if (label) audioEngine.playKey(code, label, 'release'); + rescheduleAudio(); + }; + window.addEventListener('keydown', onKeyDown); - return () => window.removeEventListener('keydown', onKeyDown); + window.addEventListener('keyup', onKeyUp); + return () => { + window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('keyup', onKeyUp); + }; }); onDestroy(() => { @@ -237,7 +260,7 @@ value={app.activeSwitch} onchange={(event) => { setActiveSwitch(event.currentTarget.value as typeof app.activeSwitch); - rescheduleAudio(); + void audioEngine.preloadActiveSwitch().then(() => rescheduleAudio()); }} > {#each SWITCH_OPTIONS as option} diff --git a/frontend/src/lib/audio/engine.ts b/frontend/src/lib/audio/engine.ts index 7d6dfef..407708d 100644 --- a/frontend/src/lib/audio/engine.ts +++ b/frontend/src/lib/audio/engine.ts @@ -1,42 +1,92 @@ +import { + markerReleaseTime, + resolvePressSampleKey, + resolveReleaseSampleKey, + sampleUrl, + type SoundPhase, + type SwitchManifest, +} from './keySoundMap'; import type { Marker, SwitchType } from '../types'; import { app } from '../store.svelte'; -const SAMPLE_PATHS: Record = { - 'cherry-mx-blue': '/assets/samples/cherry-mx-blue.wav', - 'cherry-mx-red': '/assets/samples/cherry-mx-red.wav', - 'cherry-mx-brown': '/assets/samples/cherry-mx-brown.wav', -}; - export type AudioEngine = { init: (video: HTMLVideoElement) => Promise; reschedule: (currentTime: number, markers: Marker[], isPlaying: boolean) => void; + playKey: (code: string, keyLabel: string, phase: SoundPhase) => void; + preloadActiveSwitch: () => Promise; destroy: () => void; }; +type Manifest = Record; + export function createAudioEngine(): AudioEngine { let ctx: AudioContext | null = null; let videoSource: MediaElementAudioSourceNode | null = null; let videoGain: GainNode | null = null; - const buffers = new Map(); + let sfxGain: GainNode | null = null; + let manifest: Manifest | null = null; + let loadedSwitch: SwitchType | null = null; + const buffers = new Map(); const scheduledSources: AudioBufferSourceNode[] = []; async function ensureContext() { - if (!ctx) ctx = new AudioContext(); + if (!ctx) { + ctx = new AudioContext(); + sfxGain = ctx.createGain(); + sfxGain.connect(ctx.destination); + } if (ctx.state === 'suspended') await ctx.resume(); return ctx; } - async function loadBuffer(switchType: SwitchType): Promise { - const cached = buffers.get(switchType); + async function ensureManifest() { + if (manifest) return manifest; + const response = await fetch('/assets/samples/kbsim/manifest.json'); + manifest = (await response.json()) as Manifest; + return manifest; + } + + function bufferKey(switchType: SwitchType, phase: SoundPhase, sampleKey: string): string { + return `${switchType}/${phase}/${sampleKey}`; + } + + async function loadBuffer( + switchType: SwitchType, + phase: SoundPhase, + sampleKey: string, + ): Promise { + const key = bufferKey(switchType, phase, sampleKey); + const cached = buffers.get(key); if (cached) return cached; + const audioCtx = await ensureContext(); - const response = await fetch(SAMPLE_PATHS[switchType]); + const response = await fetch(sampleUrl(switchType, phase, sampleKey)); + if (!response.ok) { + throw new Error(`Sample not found: ${sampleUrl(switchType, phase, sampleKey)}`); + } const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer); - buffers.set(switchType, audioBuffer); + buffers.set(key, audioBuffer); return audioBuffer; } + async function preloadSwitch(switchType: SwitchType) { + const data = await ensureManifest(); + const entry = data[switchType]; + if (!entry) throw new Error(`Unknown switch: ${switchType}`); + + const loads: Promise[] = []; + for (const sampleKey of entry.press) { + loads.push(loadBuffer(switchType, 'press', sampleKey)); + } + for (const sampleKey of entry.release) { + if (sampleKey === 'GENERIC_long') continue; + loads.push(loadBuffer(switchType, 'release', sampleKey)); + } + await Promise.all(loads); + loadedSwitch = switchType; + } + function clearScheduled() { for (const source of scheduledSources) { try { @@ -48,6 +98,41 @@ export function createAudioEngine(): AudioEngine { scheduledSources.length = 0; } + function playBuffer(buffer: AudioBuffer, when = 0) { + if (!ctx || !sfxGain) return; + const source = ctx.createBufferSource(); + source.buffer = buffer; + source.connect(sfxGain); + source.start(when); + return source; + } + + async function resolveAndPlay( + switchType: SwitchType, + code: string, + keyLabel: string, + phase: SoundPhase, + when = 0, + ) { + const data = await ensureManifest(); + const entry = data[switchType]; + if (!entry) return null; + + const sampleKey = + phase === 'press' + ? resolvePressSampleKey(keyLabel, code, entry.press) + : resolveReleaseSampleKey(keyLabel, entry.release); + + if (phase === 'release' && sampleKey === 'GENERIC_long') return null; + + if (loadedSwitch !== switchType) { + await preloadSwitch(switchType); + } + + const buffer = await loadBuffer(switchType, phase, sampleKey); + return playBuffer(buffer, when); + } + return { async init(video: HTMLVideoElement) { const audioCtx = await ensureContext(); @@ -57,9 +142,8 @@ export function createAudioEngine(): AudioEngine { videoSource.connect(videoGain); videoGain.connect(audioCtx.destination); } - await Promise.all( - (Object.keys(SAMPLE_PATHS) as SwitchType[]).map((id) => loadBuffer(id)), - ); + await ensureManifest(); + await preloadSwitch(app.activeSwitch); }, reschedule(time: number, markers: Marker[], playing: boolean) { @@ -71,28 +155,60 @@ export function createAudioEngine(): AudioEngine { clearScheduled(); const audioCtx = ctx; const startAt = audioCtx.currentTime + 0.02; - const buffer = buffers.get(app.activeSwitch); - if (!buffer) return; + const switchType = app.activeSwitch; - for (const marker of markers) { - if (marker.time < time - 0.05) continue; - const delay = marker.time - time; - const source = audioCtx.createBufferSource(); - source.buffer = buffer; - source.connect(audioCtx.destination); - source.start(startAt + Math.max(0, delay)); - scheduledSources.push(source); - } + void (async () => { + const data = await ensureManifest(); + const entry = data[switchType]; + if (!entry) return; + if (loadedSwitch !== switchType) { + await preloadSwitch(switchType); + } + + for (const marker of markers) { + const code = marker.code ?? marker.key; + const pressKey = resolvePressSampleKey(marker.key, code, entry.press); + const releaseKey = resolveReleaseSampleKey(marker.key, entry.release); + const releaseTime = markerReleaseTime(marker); + + if (marker.time >= time - 0.05) { + const pressDelay = marker.time - time; + const pressBuffer = await loadBuffer(switchType, 'press', pressKey); + const pressSource = playBuffer(pressBuffer, startAt + Math.max(0, pressDelay)); + if (pressSource) scheduledSources.push(pressSource); + } + + if (releaseTime >= time - 0.05) { + const releaseDelay = releaseTime - time; + const releaseBuffer = await loadBuffer(switchType, 'release', releaseKey); + const releaseSource = playBuffer(releaseBuffer, startAt + Math.max(0, releaseDelay)); + if (releaseSource) scheduledSources.push(releaseSource); + } + } + })(); + }, + + playKey(code: string, keyLabel: string, phase: SoundPhase) { + void resolveAndPlay(app.activeSwitch, code, keyLabel, phase); + }, + + async preloadActiveSwitch() { + await ensureManifest(); + await preloadSwitch(app.activeSwitch); }, destroy() { clearScheduled(); videoSource?.disconnect(); videoGain?.disconnect(); + sfxGain?.disconnect(); void ctx?.close(); ctx = null; videoSource = null; videoGain = null; + sfxGain = null; + manifest = null; + loadedSwitch = null; buffers.clear(); }, }; diff --git a/frontend/src/lib/audio/keySoundMap.ts b/frontend/src/lib/audio/keySoundMap.ts new file mode 100644 index 0000000..ca0d5ac --- /dev/null +++ b/frontend/src/lib/audio/keySoundMap.ts @@ -0,0 +1,83 @@ +import type { SwitchType } from '../types'; + +export type SoundPhase = 'press' | 'release'; +export type PressSampleKey = + | 'SPACE' + | 'ENTER' + | 'BACKSPACE' + | 'GENERIC_R0' + | 'GENERIC_R1' + | 'GENERIC_R2' + | 'GENERIC_R3' + | 'GENERIC_R4'; +export type ReleaseSampleKey = 'SPACE' | 'ENTER' | 'BACKSPACE' | 'GENERIC'; + +export type SwitchManifest = { + press: string[]; + release: string[]; +}; + +export const DEFAULT_RELEASE_OFFSET = 0.08; + +const SPECIAL_KEY_SAMPLES: Record = { + Space: 'SPACE', + Enter: 'ENTER', + Backspace: 'BACKSPACE', +}; + +/** djb2 hash — mirrored in backend/key_sound_map.py */ +export function hashCode(value: string): number { + let hash = 5381; + for (let i = 0; i < value.length; i++) { + hash = (hash * 33) ^ value.charCodeAt(i); + } + return hash >>> 0; +} + +export function pressVariantIndex(code: string): number { + return hashCode(code) % 5; +} + +export function pressVariantKey(code: string): PressSampleKey { + return `GENERIC_R${pressVariantIndex(code)}` as PressSampleKey; +} + +export function resolvePressSampleKey( + keyLabel: string, + code: string, + available: string[], +): PressSampleKey { + const special = SPECIAL_KEY_SAMPLES[keyLabel]; + if (special && available.includes(special)) return special; + const variant = pressVariantKey(code); + if (available.includes(variant)) return variant; + const fallback = `GENERIC_R${pressVariantIndex(code)}`; + if (available.includes(fallback)) return fallback as PressSampleKey; + const generic = available.find((s) => s.startsWith('GENERIC_R')); + return (generic ?? available[0]) as PressSampleKey; +} + +export function resolveReleaseSampleKey( + keyLabel: string, + available: string[], +): ReleaseSampleKey { + const special = SPECIAL_KEY_SAMPLES[keyLabel]; + if (special && available.includes(special)) return special; + if (available.includes('GENERIC')) return 'GENERIC'; + return (available[0] ?? 'GENERIC') as ReleaseSampleKey; +} + +export function sampleUrl( + switchType: SwitchType, + phase: SoundPhase, + sampleKey: string, +): string { + return `/assets/samples/kbsim/${switchType}/${phase}/${sampleKey}.mp3`; +} + +export function markerReleaseTime(marker: { + time: number; + releaseTime?: number; +}): number { + return marker.releaseTime ?? marker.time + DEFAULT_RELEASE_OFFSET; +} diff --git a/frontend/src/lib/keyboard.ts b/frontend/src/lib/keyboard.ts index 24093ee..baa7e7e 100644 --- a/frontend/src/lib/keyboard.ts +++ b/frontend/src/lib/keyboard.ts @@ -1,24 +1,5 @@ -const MODIFIER_KEYS = new Set([ - 'Control', - 'Shift', - 'Alt', - 'Meta', - 'CapsLock', - 'Tab', - 'Escape', -]); - -const IGNORED_KEYS = new Set([ - ...MODIFIER_KEYS, - 'ArrowUp', - 'ArrowDown', - 'ArrowLeft', - 'ArrowRight', - 'Backspace', - 'Delete', - 'Enter', - ' ', -]); +/** Keys that are modifiers only — never annotated on their own. */ +const MODIFIER_ONLY_KEYS = new Set(['Control', 'Shift', 'Alt', 'Meta', 'CapsLock']); export function isEditableTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; @@ -26,9 +7,17 @@ export function isEditableTarget(target: EventTarget | null): boolean { return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || target.isContentEditable; } +export function keyCode(event: KeyboardEvent): string { + return event.code; +} + +export function hasShortcutModifier(event: KeyboardEvent): boolean { + return event.ctrlKey || event.metaKey; +} + export function keyLabel(event: KeyboardEvent): string | null { - if (event.ctrlKey && event.code === 'Space') return null; - if (IGNORED_KEYS.has(event.key)) return null; + if (hasShortcutModifier(event)) return null; + if (MODIFIER_ONLY_KEYS.has(event.key)) return null; if (event.key.length === 1) return event.key; if (event.key === ' ') return 'Space'; return event.key; @@ -36,12 +25,18 @@ export function keyLabel(event: KeyboardEvent): string | null { export function shouldOverrideSelected(event: KeyboardEvent, hasSelection: boolean): boolean { if (!hasSelection) return false; - if (event.ctrlKey || event.metaKey || event.altKey) return false; + if (hasShortcutModifier(event)) return false; if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return false; return keyLabel(event) !== null; } export function shouldRecordMarker(event: KeyboardEvent): boolean { - if (event.ctrlKey || event.metaKey || event.altKey) return false; + if (event.repeat) return false; + if (hasShortcutModifier(event)) return false; return keyLabel(event) !== null; } + +export function shouldDeleteSelected(event: KeyboardEvent, hasSelection: boolean): boolean { + if (!hasSelection) return false; + return event.key === 'Backspace' || event.key === 'Delete'; +} diff --git a/frontend/src/lib/project.ts b/frontend/src/lib/project.ts index 3fd6b80..13f358f 100644 --- a/frontend/src/lib/project.ts +++ b/frontend/src/lib/project.ts @@ -1,9 +1,30 @@ -import type { Project } from './types'; +import type { LegacySwitchType, Marker, Project, SwitchType } from './types'; import { app, setActiveSwitch, setMarkers, setSelectedIds } from './store.svelte'; +const LEGACY_SWITCH_MAP: Record = { + 'cherry-mx-blue': 'mxblue', + 'cherry-mx-red': 'mxblack', + 'cherry-mx-brown': 'mxbrown', +}; + +function migrateSwitch(value: string): SwitchType { + if (value in LEGACY_SWITCH_MAP) { + return LEGACY_SWITCH_MAP[value as LegacySwitchType]; + } + return value as SwitchType; +} + +function migrateMarker(marker: Marker): Marker { + return { + ...marker, + code: marker.code, + releaseTime: marker.releaseTime, + }; +} + export function buildProject(): Project { return { - version: 1, + version: 2, switch: app.activeSwitch, markers: app.markers.map((m) => ({ ...m })), }; @@ -20,12 +41,12 @@ export function downloadProject(filename = 'project.json') { URL.revokeObjectURL(url); } -export function loadProject(data: Project) { - if (data.version !== 1) { +export function loadProject(data: Project & { version?: number; switch?: string }) { + if (data.version !== 1 && data.version !== 2) { throw new Error(`Unsupported project version: ${data.version}`); } - setMarkers(data.markers.map((m) => ({ ...m }))); - setActiveSwitch(data.switch); + setMarkers((data.markers ?? []).map(migrateMarker)); + setActiveSwitch(migrateSwitch(data.switch ?? 'mxbrown')); setSelectedIds(new Set()); } diff --git a/frontend/src/lib/store.svelte.ts b/frontend/src/lib/store.svelte.ts index cb5e609..ff5c5ae 100644 --- a/frontend/src/lib/store.svelte.ts +++ b/frontend/src/lib/store.svelte.ts @@ -10,7 +10,7 @@ export const app = $state({ fps: 30, pixelsPerSecond: ZOOM_DEFAULT, scrollLeft: 0, - activeSwitch: 'cherry-mx-blue' as SwitchType, + activeSwitch: 'mxbrown' as SwitchType, videoUrl: null as string | null, videoName: null as string | null, timelineWidth: 800, @@ -68,13 +68,16 @@ export function setTimelineWidth(value: number) { app.timelineWidth = value; } -export function addMarker(time: number, key: string): Marker { - const marker: Marker = { id: createMarkerId(), time, key }; +export function addMarker(time: number, key: string, code?: string): Marker { + const marker: Marker = { id: createMarkerId(), time, key, code }; app.markers = [...app.markers, marker].sort((a, b) => a.time - b.time); return marker; } -export function updateMarker(id: string, patch: Partial>) { +export function updateMarker( + id: string, + patch: Partial>, +) { app.markers = app.markers .map((m) => (m.id === id ? { ...m, ...patch } : m)) .sort((a, b) => a.time - b.time); diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index fd6dc76..378244a 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -1,9 +1,27 @@ -export type SwitchType = 'cherry-mx-blue' | 'cherry-mx-red' | 'cherry-mx-brown'; +export type SwitchType = + | 'cream' + | 'holypanda' + | 'alpaca' + | 'turquoise' + | 'inkblack' + | 'inkred' + | 'mxblack' + | 'mxbrown' + | 'mxblue' + | 'boxnavy' + | 'buckling' + | 'alpsblue' + | 'topre'; + +/** @deprecated v1 project switch ids — migrated on load */ +export type LegacySwitchType = 'cherry-mx-blue' | 'cherry-mx-red' | 'cherry-mx-brown'; export interface Marker { id: string; time: number; key: string; + code?: string; + releaseTime?: number; } export interface Project { @@ -13,9 +31,19 @@ export interface Project { } export const SWITCH_OPTIONS: { id: SwitchType; label: string }[] = [ - { id: 'cherry-mx-blue', label: 'Cherry MX Blue (clicky)' }, - { id: 'cherry-mx-red', label: 'Cherry MX Red (linear)' }, - { id: 'cherry-mx-brown', label: 'Cherry MX Brown (tactile)' }, + { id: 'cream', label: 'NovelKeys Creams' }, + { id: 'holypanda', label: 'Holy Pandas' }, + { id: 'alpaca', label: 'Alpacas' }, + { id: 'turquoise', label: 'Turquoise Tealios' }, + { id: 'inkblack', label: 'Gateron Black Inks' }, + { id: 'inkred', label: 'Gateron Red Inks' }, + { id: 'mxblack', label: 'Cherry MX Blacks' }, + { id: 'mxbrown', label: 'Cherry MX Browns' }, + { id: 'mxblue', label: 'Cherry MX Blues' }, + { id: 'boxnavy', label: 'Kailh Box Navies' }, + { id: 'buckling', label: 'Buckling Spring' }, + { id: 'alpsblue', label: 'SKCM Blue Alps' }, + { id: 'topre', label: 'Topre' }, ]; export const ZOOM_MIN = 20; diff --git a/scripts/fetch_kbsim_samples.sh b/scripts/fetch_kbsim_samples.sh new file mode 100755 index 0000000..af92aaf --- /dev/null +++ b/scripts/fetch_kbsim_samples.sh @@ -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" diff --git a/scripts/generate_samples.py b/scripts/generate_samples.py deleted file mode 100644 index 85c6e33..0000000 --- a/scripts/generate_samples.py +++ /dev/null @@ -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(" 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()