isaac chan · macos · audio shipped · v1.0.0

A mute for an app that doesn't have one

FreeSlapMac 1.1.0 cannot be silenced through its own interface. The slap sounds are not played by the app at all — they are 267 MP3s played by the bundled spank process, and neither side exposes a volume or a mute. So this builds a copy of the app whose entire sound library is digital silence at the original duration, then decodes every clip back through macOS's own AudioToolbox to prove it contains zero non-zero bytes. One bash script. No python, no brew, no ffmpeg.

clips silenced
267 / 267
non-zero PCM bytes
0
duration drift
0.00 s
dependencies
none
app size
15 → 14 MB
detection / flash
unchanged
⬇ Download v1.0.0build.sh · verify.sh · switch.sh · README · MIT Read every file (mirror) The other missing half
the brief “also how do i turn off creaking and tap sound” — then, after being told the tap sound has no off switch: “how about you build one that works.”

01Two sounds, two completely different mechanisms

The lid creak and the slap taps feel like the same setting. They are not. The creak is a helper I wrote reading the lid-angle sensor and playing a sound — it honours the app's _creakingEnabled preference, so one switch silences it.

The taps are different. Pull the app apart and the audio simply isn't there:

$ defaults read com.devesh.freeslapmac
{
    launchAtLogin   = 1;
    screenFlash     = 1;
    sensitivity     = "0.3427";
    triggerMode     = woman;
    _creakingEnabled = 1;
}
# five preferences. no volume. no soundEnabled. nothing to mute.

$ /Applications/FreeSlapMac.app/Contents/MacOS/spank --help
  --custom DIR   --min-amplitude F   --cooldown MS   --speed F   --volume-scaling
# the sound engine. no --mute, no --volume, no --no-audio.

The app launches spank with --custom .../Contents/Resources/Sounds/<mode> and spank plays those MP3s itself. So "turn the tap sound off" has no supported answer in 1.1.0 — not a hidden checkbox, not a default key. There is no such control anywhere in the bundle.

02The four obvious routes, and why each one fails

routewhat happens
set a volume preferenceThere is no such key. The app stores five preferences and audio is not among them.no such pref
kill spankThe root wrapper respawns it on its next loop — it keeps LAST_CMD and restarts the process.respawns
pause detectionPausing stops the app reacting. The audio was never the app's to pause — it is spank's.wrong layer
turn the Mac's volume downWorks, and silences everything else you own at the same time.global
replace the audioNothing left to play. Detection and visuals never touch the audio path, so they keep working.this build

03What "silence" actually is, byte for byte

A silent MP3 does not need an encoder. One MPEG-1 Layer III frame at 44.1 kHz mono, 128 kb/s is 417 bytes and carries 1152 samples. Its four header bytes are FF FB 90 C0; the side information and main data that follow are all zero, and all-zero spectral data decodes to exact digital silence. So the build emits that frame as often as needed:

# one frame, 26.122 ms
printf '\xff\xfb\x90\xc0' > frame; head -c 413 /dev/zero >> frame

# per clip: match the original duration to the nearest frame, then truncate
dur=$(afinfo "$clip" | awk -F': ' '/estimated duration/{print $2}')
frames=$(awk -v d="$dur" 'BEGIN{printf "%d", d*1000/26.122448979591837 + 0.5}')
head -c $((frames*417)) silence.mp3 > "$clip"

Duration is matched rather than flattened to a fixed length on purpose: the clap, the punch and the laugh are different lengths, and a silent build that changes clip lengths is a silent build that changes timing. Everything else is a straight copy — ditto, rename it, and re-sign, because touching resources breaks the original seal. That is cheap here: the shipped app is Signature=adhoc already, so the copy is ad-hoc signed the same way and inherits the exact same trust posture.

$ codesign -dv /Applications/FreeSlapMac.app
Signature=adhoc                      # the original, before I touched anything

$ bash build.sh
copying /Applications/FreeSlapMac.app
     -> /Applications/FreeSlapMac Silent.app
silenced 267/267 MP3s, re-signed ad-hoc

04The proof, and the bug that made the proof necessary

My first version used ffmpeg and a thread pool, and every worker wrote to the same temporary file. Twenty-three of the 267 clips came out corrupted — and the verifier said so instead of assuming success. That is the entire argument for writing the check first:

# first build, before the fix
files silenced: 267/267
silent (peak ≤ -91 dBFS): 244/267
undecodable: 23            # shared temp file, race

# after unique temp paths — and after dropping ffmpeg entirely

Then I threw the dependency away: the frame above is pure bash, and the check is afconvert, which decodes through the same AudioToolbox that spank uses to play the file. Zero non-zero bytes is not "quiet" — it is the absence of sound, and it is the only result the script accepts:

$ bash verify.sh
== files ==
  original: 267 mp3   silent: 267 mp3
== decode check (AudioToolbox) ==
  decoded: 267   digitally silent: 267   audible: 0   duration mismatches: 0
== signature / identity ==
  signature: valid (ad-hoc)
  bundle id: com.devesh.freeslapmac (settings carry over)
  display name: FreeSlapMac Silent
  executable: identical code (901aecc6a53907cd…)
== size ==
  original  15M   silent  14M
PASS — every clip decodes to digital silence, signature valid, same code and settings.

That last line matters twice: identical code means the executable is byte-for-byte the original (only the signature blob differs), and same bundle id means your sensitivity, sound mode, cooldown and lid-creak preference carry straight over instead of resetting.

what still works

Slap detection, screen flash, combo counter, slap count, hot corners, settings — none of it goes through the audio path, so all of it behaves exactly as before. The fork is the same binary with a silent library wired to it.

05Limits, honestly

06The thing I found while reading its launch path

To silence the sound I had to know who plays it, so I read the wrapper the app writes at launch. It runs as root and it does this:

FIFO="/tmp/freeslapmac_cmd"
...
chmod 666 "$FIFO"                    # world-writable
...
start_spank() {
    eval "$1" >> "$OUTPUT" 2>&1 &   # as root
}
...
if read -t 1 CMD < "$FIFO"; then
    start_spank "$CMD"

Any local process — or any other user on the machine — can write a command string into that FIFO and have it executed as root while the app is running. That is not something this build needs, and using it would have been the fastest way to build the mute: one line to the FIFO would restart the sound engine against a silent folder. I didn't, for the obvious reason — a mute that depends on a local root-execution channel is a worse problem than the sound. It is worth reporting upstream, and worth not leaving either build running on a shared or guest account in the meantime.

if you install this

First launch asks for your admin password once, exactly like the original — spank needs it to read the accelerometer. Settings are shared with the loud build, so if you keep both, remember they are the same preferences: change your sensitivity in one and the other follows.

07What is in the package

filewhat it does
build.shCopies the app, silences all 267 MP3s at matched duration, renames it, re-signs ad-hoc, strips quarantine, then runs the verifier. Positional args for source and destination; FORCE=1 to replace an existing build.
verify.shDecodes every clip through AudioToolbox and fails unless each one is 100 % zero PCM and within 60 ms of the original duration. Also checks the signature, the bundle id and that the executable is unchanged.
switch.shsilent / loud — quits one build politely (so its root wrapper tears down) and opens the other.
README.mdThe same argument as this page, with the commands, the expected verifier output and the limitations.