# Build spec — FreeSlapMac Silent (v1.0.0)

## Goal

Make it possible to run FreeSlapMac — and keep every bit of its behaviour except the noise — when the app
itself has no mute. Ship it so a stranger can build it from their own copy with no dependencies.

## The constraint that shapes everything

The slap/tap sounds are not the app's. `spank`, the bundled sound engine, plays MP3s from
`Contents/Resources/Sounds/` and takes no volume or mute argument:

```
--custom DIR  --min-amplitude F  --cooldown MS  --speed F  --volume-scaling
```

The app stores exactly five preferences — `sensitivity`, `triggerMode`, `screenFlash`, `launchAtLogin`,
`_creakingEnabled` — and none of them is audio. There is no supported off switch anywhere in 1.1.0.

## Approaches considered, and rejected

1. **Pause detection.** Wrong layer: it stops the app reacting, but the sound was never the app's to pause.
2. **Kill `spank`.** The root wrapper respawns it from `LAST_CMD` on the next loop iteration.
3. **System volume.** Silences the entire machine, not the app.
4. **Rewrite the app's audio path.** Binary patch, resigned periodically by the vendor — brittle forever.
5. **Write to `/tmp/freeslapmac_cmd`.** The root wrapper `eval`s whatever lands there (see "Security" below).
   It would work in one line and it is the reason *not* to: a mute built on a local root-execution channel is
   a worse bug than the sound.
6. **Replace the audio library.** Chosen. Detection and visuals never touch the audio path.

## Design

- `ditto` copy the installed bundle; never modify the original.
- Replace every MP3 with a silent MPEG-1 Layer III frame at the original duration:
  header `FF FB 90 C0` + zero side-info + zero main data = 417 bytes per 1152-sample frame (26.122 ms).
- Duration matched to the original clip within a frame, so clip timing does not change.
- Rename `CFBundleName` / `CFBundleDisplayName` to "FreeSlapMac Silent"; **keep `CFBundleIdentifier`** so the
  user's sensitivity, mode, cooldown and creak preference carry over.
- Re-sign ad-hoc. The original is `Signature=adhoc`, so this reproduces its exact trust posture rather than
  introducing a weaker one.
- Strip `com.apple.quarantine` — it is a local build, not a download.

## Dependencies: none

`afinfo`, `afconvert`, `codesign`, `ditto`, `PlistBuddy` all ship with macOS. The first build used `ffmpeg`
and was rewritten specifically to drop it: a tool that asks a stranger to `brew install` something before it
can un-mute their own laptop is a tool that will not get used.

## Verification requirements (what the package must prove)

- Every clip decodes through AudioToolbox (`afconvert`) to **zero non-zero PCM bytes** — not "quiet".
- Duration of every silent clip within 60 ms of the original.
- `codesign --verify --deep --strict` passes.
- Bundle id unchanged.
- Executable byte-identical to the original once signatures are stripped (only the signature blob may differ).
- File count identical (267 → 267).
- A fresh-copy run with arbitrary source/destination paths, and a refusal to overwrite without `FORCE=1`.

## Failure that must be reported, not hidden

The first implementation used a thread pool with one shared temp filename; 23 of 267 files came out
undecodable. The verifier caught it. The package must keep failing loudly on any clip that is not pure
silence — a silent build that is 97 % silent is a bug, not a release.

## Security note to publish

The app's launch wrapper runs as root, chmods `/tmp/freeslapmac_cmd` to `666`, and `eval`s whatever any local
process writes into it. Document it on the page; do not build the tool on top of it.

## Non-goals

- Not a fix for the app, and not a report to the vendor (though it should be reported).
- Does not silence the lid-creak helper — that is a separate preference.
- Never redistributes the app or its audio: the silence is generated locally.
