These are copies, not re-exports: each row's sha-256 is the hash of the file served here and of the same file inside the zip. Both are checked against the package before publication, and the live site is re-hashed afterwards.
zip sha-256 f937fce517256ca847d9506ec0684689cbc3d21b4d8c7b1440d23644545521fc
| file | bytes | lines | sha-256 of the served copy |
|---|---|---|---|
| build.sh | 3,827 | 80 | ea56b6d768ecca9059270e857365e385036e1ec7d3c2674f96f5916152ff7602 |
| verify.sh | 3,808 | 78 | 1c9eb94a91617f366f55bde35f1f3bec7463b4d915135e467f9bcbfdc01cb8ab |
| switch.sh | 1,106 | 31 | da038ea7e2faad2c91f1cd339c620d0401aab4fedd0d56462b46cc3db38d482f |
| README.md | 4,311 | 105 | 73200526b52ebf46e26b3ba1e327ac66c8c4f755607de96bb3fc2eb5468ad989 |
| LICENSE | 1,067 | 21 | 5d963dad33771425d4e62016aff764d49296198333f3c104ace6ca710208feb4 |
#!/bin/bash
# freeslapmac-silent — build a silent copy of FreeSlapMac.
#
# FreeSlapMac has no mute: its slap/tap sounds are MP3s inside
# Contents/Resources/Sounds/, played by the bundled `spank` process. This script copies
# the app and overwrites every MP3 with a hand-built silent MPEG frame of the same
# duration, then ad-hoc re-signs the copy (the original is ad-hoc signed too).
# Detection, screen flash, combos, counters and settings are untouched.
#
# Usage: bash build.sh [SOURCE_APP] [DEST_APP]
# Default: bash build.sh "/Applications/FreeSlapMac.app" "/Applications/FreeSlapMac Silent.app"
# Needs: macOS only. No python, no brew, no ffmpeg — afinfo/afconvert/codesign ship with macOS.
set -euo pipefail
SRC="${1:-/Applications/FreeSlapMac.app}"
DEST="${2:-/Applications/FreeSlapMac Silent.app}"
SOUNDS_REL="Contents/Resources/Sounds"
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
[ -d "$SRC" ] || die "source app not found: $SRC"
[ -d "$SRC/$SOUNDS_REL" ] || die "not a FreeSlapMac bundle (no $SOUNDS_REL): $SRC"
[ "$(cd "$SRC" && pwd -P)" != "$(cd "$DEST" 2>/dev/null && pwd -P || echo -)" ] || die "source and destination are the same app"
if [ -e "$DEST" ] && [ "${FORCE:-0}" != "1" ]; then
die "destination already exists: $DEST (re-run with FORCE=1 to replace it)"
fi
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
# One silent MPEG-1 Layer III frame: 44.1 kHz, mono, 128 kb/s = 417 bytes, 1152 samples
# (26.122 ms). Zeroed side-info + zeroed main data decode to exact digital silence.
printf '\xff\xfb\x90\xc0' > "$TMP/frame"
head -c 413 /dev/zero >> "$TMP/frame"
[ "$(wc -c < "$TMP/frame" | tr -d ' ')" = "417" ] || die "frame build failed"
FRAME_MS=26.122448979591837
# 3000 frames (78 s) covers every clip in the shipped library with room to spare.
: > "$TMP/silence.mp3"
args=(); for ((i = 0; i < 3000; i++)); do args+=("$TMP/frame"); done
cat "${args[@]}" > "$TMP/silence.mp3"
printf 'copying %s\n -> %s\n' "$SRC" "$DEST"
rm -rf "$DEST"
ditto "$SRC" "$DEST"
total=0 replaced=0
while IFS= read -r -d '' f; do
total=$((total + 1))
dur="$(afinfo "$f" 2>/dev/null | awk -F': ' '/estimated duration/ {print $2; exit}' | awk '{print $1}')"
if [ -z "$dur" ]; then
printf 'warn: no duration for %s — using 1.0 s\n' "$f" >&2
dur=1.0
fi
frames="$(awk -v d="$dur" -v m="$FRAME_MS" 'BEGIN { n = int(d * 1000 / m + 0.5); if (n < 1) n = 1; print n }')"
head -c $((frames * 417)) "$TMP/silence.mp3" > "$f.new" && mv "$f.new" "$f"
replaced=$((replaced + 1))
done < <(find "$DEST/$SOUNDS_REL" -name '*.mp3' -print0)
[ "$replaced" -gt 0 ] || die "no MP3s found to silence under $DEST/$SOUNDS_REL"
# Distinguish it in the menu bar / Dock. Bundle id is deliberately preserved so the
# silent build inherits your sensitivity, mode, cooldown and creak preferences.
PB=/usr/libexec/PlistBuddy
INFO="$DEST/Contents/Info.plist"
$PB -c "Set :CFBundleName FreeSlapMac Silent" "$INFO" 2>/dev/null || $PB -c "Add :CFBundleName string FreeSlapMac Silent" "$INFO"
$PB -c "Set :CFBundleDisplayName FreeSlapMac Silent" "$INFO" 2>/dev/null || $PB -c "Add :CFBundleDisplayName string FreeSlapMac Silent" "$INFO"
# Modifying resources breaks the original seal — re-sign ad-hoc, exactly like the original.
codesign --force --deep --sign - "$DEST" >/dev/null 2>&1 || die "codesign failed"
xattr -cr "$DEST" 2>/dev/null || true
printf 'silenced %d/%d MP3s, re-signed ad-hoc\n' "$replaced" "$total"
if [ -x "$(dirname "$0")/verify.sh" ] || [ -f "$(dirname "$0")/verify.sh" ]; then
printf '\nverifying…\n'
bash "$(dirname "$0")/verify.sh" "${VERIFY_SRC:-$SRC}" "$DEST"
else
printf '\n(verify.sh not found next to build.sh — skipping the proof step)\n'
fi
printf '\nDone. Launch with:\n open -a "%s"\nRun only one build at a time — both write /tmp/freeslapmac_cmd.\n' "$DEST"
#!/bin/bash
# freeslapmac-silent — prove the silent build really is silent.
#
# For every MP3 in the destination bundle this decodes the file with macOS's own
# AudioToolbox (afconvert) — the same decoder `spank` uses to play it — and asserts the
# PCM payload contains no non-zero byte at all, and that its duration still matches the
# original clip. Also checks the signature, the bundle id and the executable.
#
# Usage: bash verify.sh [SOURCE_APP] [DEST_APP]
set -uo pipefail
SRC="${1:-/Applications/FreeSlapMac.app}"
DEST="${2:-/Applications/FreeSlapMac Silent.app}"
SOUNDS_REL="Contents/Resources/Sounds"
fail=0
say() { printf '%s\n' "$*"; }
bad() { printf ' ✗ %s\n' "$*"; fail=$((fail + 1)); }
[ -d "$SRC" ] || { echo "no source app: $SRC" >&2; exit 2; }
[ -d "$DEST" ] || { echo "no destination app: $DEST" >&2; exit 2; }
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
dur() { afinfo "$1" 2>/dev/null | awk -F': ' '/estimated duration/ {print $2; exit}' | awk '{print $1}'; }
say "== files =="
srcfiles=(); while IFS= read -r l; do srcfiles+=("$l"); done < <(find "$SRC/$SOUNDS_REL" -name '*.mp3' | sort)
dstfiles=(); while IFS= read -r l; do dstfiles+=("$l"); done < <(find "$DEST/$SOUNDS_REL" -name '*.mp3' | sort)
say " original: ${#srcfiles[@]} mp3 silent: ${#dstfiles[@]} mp3"
[ "${#srcfiles[@]}" -eq "${#dstfiles[@]}" ] && [ "${#dstfiles[@]}" -gt 0 ] || bad "file count differs"
say "== decode check (AudioToolbox) =="
n=0 silent=0 loud=0 durdrift=0
for f in "${dstfiles[@]}"; do
n=$((n + 1))
wav="$TMP/x.wav"
if ! afconvert -f WAVE -d LEI16 "$f" "$wav" 2>/dev/null; then bad "cannot decode $f"; continue; fi
off="$(LC_ALL=C grep -abo 'data' "$wav" | head -1 | cut -d: -f1)"
[ -n "$off" ] || { bad "no data chunk in $f"; continue; }
nz="$(tail -c +$((off + 9)) "$wav" | LC_ALL=C tr -d '\000' | wc -c | tr -d ' ')"
if [ "$nz" = "0" ]; then silent=$((silent + 1)); else loud=$((loud + 1)); bad "audible content in $f ($nz non-zero bytes)"; fi
rel="${f#"$DEST"/}"
if [ -f "$SRC/$rel" ]; then
d1="$(dur "$f")"; d2="$(dur "$SRC/$rel")"
awk -v a="$d1" -v b="$d2" 'BEGIN { exit (a - b < 0.10 && b - a < 0.10) ? 0 : 1 }' || { durdrift=$((durdrift + 1)); bad "duration drift $rel: $d2 s -> $d1 s"; }
fi
rm -f "$wav"
done
say " decoded: $n digitally silent: $silent audible: $loud duration mismatches: $durdrift"
say "== signature / identity =="
codesign --verify --deep --strict "$DEST" >/dev/null 2>&1 && say " signature: valid (ad-hoc)" || bad "signature invalid"
src_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$SRC/Contents/Info.plist")"
dst_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$DEST/Contents/Info.plist")"
[ "$src_id" = "$dst_id" ] && say " bundle id: $dst_id (settings carry over)" || bad "bundle id changed: $src_id -> $dst_id"
dst_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "$DEST/Contents/Info.plist")"
say " display name: $dst_name"
cp "$SRC/Contents/MacOS/$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$SRC/Contents/Info.plist")" "$TMP/a.bin" 2>/dev/null
cp "$DEST/Contents/MacOS/$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$DEST/Contents/Info.plist")" "$TMP/b.bin" 2>/dev/null
codesign --remove-signature "$TMP/a.bin" 2>/dev/null; codesign --remove-signature "$TMP/b.bin" 2>/dev/null
if cmp -s "$TMP/a.bin" "$TMP/b.bin"; then
say " executable: identical code ($(shasum -a 256 "$TMP/b.bin" | cut -c1-16)…)"
else
bad "executable differs from the original"
fi
say "== size =="
say " original $(du -sh "$SRC" | cut -f1) silent $(du -sh "$DEST" | cut -f1)"
if [ "$fail" -eq 0 ]; then
say "PASS — every clip decodes to digital silence, signature valid, same code and settings."
exit 0
fi
say "FAIL — $fail problem(s) above."
exit 1
#!/bin/bash
# freeslapmac-silent — switch between the loud and silent builds.
# Only one may run at a time: both write /tmp/freeslapmac_cmd, and each launch kills
# every spank process it can find.
#
# Usage: bash switch.sh silent | loud
set -uo pipefail
LOUD="${LOUD_APP:-/Applications/FreeSlapMac.app}"
SILENT="${SILENT_APP:-/Applications/FreeSlapMac Silent.app}"
case "${1:-}" in
silent) TARGET="$SILENT" ;;
loud) TARGET="$LOUD" ;;
*) echo "usage: bash switch.sh silent|loud" >&2; exit 2 ;;
esac
[ -d "$TARGET" ] || { echo "not installed: $TARGET" >&2; echo "build it first: bash build.sh" >&2; exit 1; }
# Quit politely so the app can tear down its root wrapper (a hard kill leaves it running).
osascript -e 'quit app "FreeSlapMac Silent"' >/dev/null 2>&1
osascript -e 'quit app "FreeSlapMac"' >/dev/null 2>&1
for _ in 1 2 3 4 5 6; do pgrep -x FreeSlapMac >/dev/null 2>&1 || break; sleep 0.5; done
if pgrep -x FreeSlapMac >/dev/null 2>&1; then
echo "FreeSlapMac is still running — quit it manually, then re-run this." >&2
exit 1
fi
open -a "$TARGET"
echo "launched: $TARGET"
# FreeSlapMac Silent A silent build of [FreeSlapMac](https://freeslapmac.app) — same detection, same screen flash, same combo counter, **no sound**. Plus the reason it needs to exist, which is a one-word answer: FreeSlapMac has no mute. ## Why this is necessary The slap/tap sounds are not played by the app. They are MP3s in `FreeSlapMac.app/Contents/Resources/Sounds/` played by the bundled `spank` process, and `spank` exposes no volume or mute switch: ``` $ /Applications/FreeSlapMac.app/Contents/MacOS/spank --help --custom DIR --min-amplitude F --cooldown MS --speed F --volume-scaling ``` The app itself stores only `sensitivity`, `triggerMode`, `screenFlash`, `launchAtLogin` and `_creakingEnabled` — no volume, no `soundEnabled`. So "turn the sound off" has no supported answer in 1.1.0. This does not patch the app or hook its audio: it builds a *copy* whose entire sound library is digital silence, so there is nothing to play. Detection and visuals never touch the audio path, so they keep working. ## Requirements macOS. That's it. No python, no Homebrew, no ffmpeg — it uses `afinfo`, `afconvert`, `codesign`, `ditto` and `PlistBuddy`, all of which ship with the OS. You do need FreeSlapMac installed, since the build starts from your own copy (the app is not redistributed here). ## Build ```sh bash build.sh # → /Applications/FreeSlapMac Silent.app ``` Custom locations: ```sh bash build.sh "/Applications/FreeSlapMac.app" "$HOME/Applications/FreeSlapMac Silent.app" FORCE=1 bash build.sh # replace an existing silent build ``` It copies the bundle, overwrites every MP3 with a hand-built silent MPEG frame of the same duration, renames the copy so you can tell the two apart, re-signs it ad-hoc (the original is ad-hoc signed too) and strips quarantine. ~270 files, about a minute. ## Verify it yourself ```sh bash verify.sh ``` For each clip it decodes the file with macOS's own AudioToolbox — the same decoder `spank` uses — and asserts the PCM contains no non-zero byte, that the duration still matches the original, that the signature is valid and that the executable is byte-identical to the original (only the signature blob differs). Expected output: ``` == 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 PASS — every clip decodes to digital silence, signature valid, same code and settings. ``` Zero non-zero bytes is what silence means here. A trimmed file, a sped-up file or a "quiet" file would fail. ## Use ```sh bash switch.sh silent # quit the loud build, launch the silent one bash switch.sh loud # back to sound ``` One at a time only: both builds write `/tmp/freeslapmac_cmd` and each launch kills every `spank` it finds, so two instances fight each other. First launch asks for your admin password once, exactly like the original — `spank` needs it to read the accelerometer. Settings are shared (same bundle id), so sensitivity, sound mode, cooldown and the lid-creak toggle carry over untouched. If you want the silent build at login, remove "Open at Login" from the loud one first — login items resolve by bundle id, and both builds share it. ## Uninstall ```sh rm -rf "/Applications/FreeSlapMac Silent.app" ``` The original app is never modified. ## Notes - **Lid creak is not covered.** If you use a lid-creak helper that plays its own audio (mine does), that sound does not come from this bundle; disable it in FreeSlapMac's own "Lid Creaking Sound" switch or in the helper. - **Security, unrelated to this script:** FreeSlapMac starts a *root* wrapper that `eval`s whatever any local process writes to the world-writable FIFO `/tmp/freeslapmac_cmd`. While it runs, any user on the machine can get root. This build changes none of that — don't leave either build running on a shared or guest account, and it is worth reporting upstream. - Not affiliated with FreeSlapMac. No third-party audio is redistributed: the silence is generated locally and the app itself is never shipped. MIT — see LICENSE.
MIT License Copyright (c) 2026 Isaac Chan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.