mirror · freeslapmac-lidcreakbyte-identical

Every file in v1.0.1, exactly as shipped

These are copies, not re-exports: each row's sha-256 is the hash of the file you download here and of the same file inside the zip. Both the copy in this folder and the copy served through this page are checked against the package before it is published.

⬇ Download the zip (11,601 B) Read the analysis Open the simulator

zip sha-256 25857680092575e22fe784a9c38ca8fc458bcafe635b3b425050b86a70eb51a9

filebyteslinessha-256 of the served copy
lidcreak.py13,88938513889
install.sh3,5521093552
uninstall.sh95825958
README.md6,9361466936
LICENSE1,369281369
lidcreak.py · 13,889 B · 385 lines · sha256 8deefa2135edab17…
#!/usr/bin/env python3
"""lidcreak — make FreeSlapMac's dead "Lid Creaking Sound" switch actually creak.

Reads the MacBook's hidden lid-angle sensor (IOHID usage page 0x20 / usage 0x8A,
feature report 1) and plays FreeSlapMac's Sounds/creaking.mp3 while the lid moves.

v1.0.1 — two fixes over 1.0.0:
  * no longer gated on FreeSlapMac being open (it never was the thing playing the
    creak, so requiring it only ever produced silence with the app quit),
  * real gain (1.0-2.0 via afplay -v) because creaking.mp3 is mastered ~30 dB
    below the other sounds; the old 0.35 volume was inaudible even at full system
    volume, and a muted/zero system output now gets reported instead of ignored.

Gated by FreeSlapMac's own toggle (pref _creakingEnabled in com.devesh.freeslapmac),
so the switch inside the app really does control it.

No root, no kext, no code signature changes: feature-report reads are unprivileged.

Usage:
    lidcreak.py              run the daemon (what launchd does)
    lidcreak.py --self-test  sensor + audio report, play one creak
    lidcreak.py --prove      play three creaks, quiet -> loud, to pick a level
    lidcreak.py --trigger-test  exercise the trigger logic offline (no audio)
    lidcreak.py --watch      print live angle + speed to stdout (tuning mode)

Options:
    --app PATH        FreeSlapMac.app location  (default: auto-detect)
    --trigger DEG_S   lid speed that counts as a creak (default 8.0 deg/s)
    --volume GAIN     fixed gain 1.0-2.0 (default: 1.0 + speed/40, capped 2.0)
    --sound PATH      override the mp3
"""
import ctypes
import ctypes.util
import os
import subprocess
import sys
import time

VERSION = "1.0.1"

BUNDLE = "com.devesh.freeslapmac"
PREF_KEY = "_creakingEnabled"

PAGE, USAGE = 0x20, 0x8A          # lid angle sensor
POLL_HZ = 20
SPEED_TRIGGER = 8.0               # deg/sec of lid movement that counts
RETRIGGER_S = 1.4
SAMPLE_GAP_MAX = 0.5              # ignore deltas across sleep/stall
GAIN_MIN, GAIN_MAX = 1.0, 2.0     # afplay -v gain (creaking.mp3 is quiet)

HOME = os.path.expanduser("~")
LOG = os.path.join(HOME, "Library/Logs/lidcreak.log")

CANDIDATE_APPS = [
    "/Applications/FreeSlapMac.app",
    os.path.join(HOME, "Applications/FreeSlapMac.app"),
    "/Applications/FreeSlapMac (free).app",
]

IOKit = ctypes.cdll.LoadLibrary(ctypes.util.find_library("IOKit"))
CF = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
kCFAllocatorDefault = ctypes.c_void_p.in_dll(CF, "kCFAllocatorDefault")

CF.CFStringCreateWithCString.restype = ctypes.c_void_p
CF.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32]
CF.CFNumberCreate.restype = ctypes.c_void_p
CF.CFNumberCreate.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
CF.CFDictionaryCreate.restype = ctypes.c_void_p
CF.CFDictionaryCreate.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p),
                                  ctypes.POINTER(ctypes.c_void_p), ctypes.c_long,
                                  ctypes.c_void_p, ctypes.c_void_p]
IOKit.IOHIDManagerCreate.restype = ctypes.c_void_p
IOKit.IOHIDManagerCreate.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
IOKit.IOHIDManagerSetDeviceMatching.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
IOKit.IOHIDManagerCopyDevices.restype = ctypes.c_void_p
IOKit.IOHIDManagerCopyDevices.argtypes = [ctypes.c_void_p]
CF.CFSetGetCount.restype = ctypes.c_long
CF.CFSetGetCount.argtypes = [ctypes.c_void_p]
CF.CFSetGetValues.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]
IOKit.IOHIDDeviceOpen.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
IOKit.IOHIDDeviceGetReport.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_long,
                                       ctypes.c_char_p, ctypes.POINTER(ctypes.c_long)]
IOKit.IOHIDDeviceClose.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
kCFNumberIntType = 9
kCFStringEncodingUTF8 = 0x08000100


def find_app():
    for p in CANDIDATE_APPS:
        if os.path.isdir(p):
            return p
    return None


def opt(flag, default):
    """--flag VALUE  (also --flag=VALUE)"""
    for i, a in enumerate(sys.argv):
        if a == flag and i + 1 < len(sys.argv):
            return sys.argv[i + 1]
        if a.startswith(flag + "="):
            return a.split("=", 1)[1]
    return default


APP = opt("--app", None) or find_app() or "/Applications/FreeSlapMac.app"
CREAK = opt("--sound", os.path.join(APP, "Contents/Resources/Sounds/creaking.mp3"))
SPEED_TRIGGER = float(opt("--trigger", SPEED_TRIGGER))
FIXED_VOLUME = opt("--volume", None)
VERBOSE = "--watch" in sys.argv


def log(msg):
    line = f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n"
    try:
        if os.path.exists(LOG) and os.path.getsize(LOG) > 512 * 1024:
            os.replace(LOG, LOG + ".1")
        with open(LOG, "a") as fh:
            fh.write(line)
    except OSError:
        pass


def cfstr(s):
    return CF.CFStringCreateWithCString(kCFAllocatorDefault, s.encode(), kCFStringEncodingUTF8)


def cfnum(i):
    v = ctypes.c_int(i)
    return CF.CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, ctypes.byref(v))


def open_lid_device():
    keys = (ctypes.c_void_p * 2)(cfstr("PrimaryUsagePage"), cfstr("PrimaryUsage"))
    vals = (ctypes.c_void_p * 2)(ctypes.cast(cfnum(PAGE), ctypes.c_void_p),
                                 ctypes.cast(cfnum(USAGE), ctypes.c_void_p))
    matching = CF.CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, None, None)
    mgr = IOKit.IOHIDManagerCreate(kCFAllocatorDefault, 0)
    IOKit.IOHIDManagerSetDeviceMatching(mgr, matching)
    devs = IOKit.IOHIDManagerCopyDevices(mgr)
    if not devs:
        return None
    n = CF.CFSetGetCount(devs)
    if n < 1:
        return None
    arr = (ctypes.c_void_p * n)()
    CF.CFSetGetValues(devs, arr)
    dev = arr[0]
    if IOKit.IOHIDDeviceOpen(dev, 0) != 0:
        return None
    return dev


def read_angle(dev):
    """Feature report 1 -> uint16 LE angle in degrees (report byte 0 = report ID)."""
    buf = ctypes.create_string_buffer(8)
    ln = ctypes.c_long(8)
    if IOKit.IOHIDDeviceGetReport(dev, 2, 1, buf, ctypes.byref(ln)) != 0:
        return None
    raw = buf.raw[: ln.value]
    if len(raw) < 3:
        return None
    return float(raw[1] | (raw[2] << 8))


def app_running():
    return subprocess.run(["pgrep", "-x", "FreeSlapMac"],
                          stdout=subprocess.DEVNULL,
                          stderr=subprocess.DEVNULL).returncode == 0


def creak_enabled():
    r = subprocess.run(["defaults", "read", BUNDLE, PREF_KEY],
                       capture_output=True, text=True)
    if r.returncode != 0:
        return True                 # never written: treat as on
    return r.stdout.strip().lower() in ("1", "true", "yes")


def audio_state():
    """(volume, muted) of the default output, or (None, None) if unreadable."""
    try:
        r = subprocess.run(["osascript", "-e", "get volume settings"],
                           capture_output=True, text=True, timeout=5)
    except (OSError, subprocess.TimeoutExpired):
        return None, None
    vol = muted = None
    for part in r.stdout.replace("\n", ",").split(","):
        part = part.strip()
        if part.startswith("output volume:"):
            try:
                vol = int(part.split(":")[1])
            except ValueError:
                pass
        elif part.startswith("output muted:"):
            muted = part.split(":")[1].strip() == "true"
    return vol, muted


def audio_ok():
    """True when the machine can actually be heard. Logs the reason when not."""
    vol, muted = audio_state()
    if muted:
        log(f"WARNING system output is MUTED - creaks play but cannot be heard "
            f"(set volume output muted false)")
        return False
    if vol == 0:
        log("WARNING system output volume is 0 - creaks play but cannot be heard")
        return False
    return True


def gain_for(speed):
    if FIXED_VOLUME:
        return float(FIXED_VOLUME)
    return min(GAIN_MAX, max(GAIN_MIN, GAIN_MIN + speed / 40.0))


_player = None


def play_creak(volume):
    global _player
    if _player is not None and _player.poll() is None:
        _player.kill()
    try:
        _player = subprocess.Popen(
            ["afplay", "-t", "1.8", "-v", f"{volume:.2f}", CREAK],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        log(f"creak vol={volume:.2f}")
        return True
    except OSError as exc:
        log(f"afplay failed: {exc}")
        return False


def should_play(speed, now, last_play, enabled):
    """Pure trigger decision -> (play, reason). Kept separate so it can be tested."""
    if speed < SPEED_TRIGGER:
        return False, "slow"
    if not enabled:
        return False, "toggle off"
    if (now - last_play) < RETRIGGER_S:
        return False, "retrigger"
    return True, "creak"


def trigger_test():
    """Offline proof the trigger fires on a real lid close and stays quiet otherwise."""
    cases = [
        ("slow drift 2 deg/s", should_play(2.0, 100.0, 0.0, True), False),
        ("normal move 20 deg/s", should_play(20.0, 100.0, 0.0, True), True),
        ("fast close 136 deg/s", should_play(136.0, 100.0, 0.0, True), True),
        ("toggle off", should_play(136.0, 100.0, 0.0, False), False),
        ("0.5s after last creak", should_play(136.0, 100.5, 100.0, True), False),
        ("2s after last creak", should_play(136.0, 102.0, 100.0, True), True),
    ]
    bad = 0
    for name, (play, reason), want in cases:
        ok = play == want
        bad += 0 if ok else 1
        print(f"{'PASS' if ok else 'FAIL'}  {name:26s} play={play!s:5s} reason={reason}")
    print(f"\n{len(cases) - bad}/{len(cases)} trigger cases pass")
    return 0 if bad == 0 else 1


def self_test():
    print(f"lidcreak {VERSION}")
    print(f"app: {APP}")
    print(f"sound: {CREAK} exists={os.path.exists(CREAK)}")
    dev = open_lid_device()
    if dev is None:
        print("LID DEVICE: not found")
        return 1
    angle = read_angle(dev)
    print(f"LID ANGLE: {angle}")
    print(f"APP RUNNING: {app_running()}  TOGGLE ON: {creak_enabled()}")
    vol, muted = audio_state()
    print(f"SYSTEM OUTPUT: volume={vol} muted={muted}"
          f"{'   <-- SILENT, nothing can be heard' if (muted or vol == 0) else ''}")
    print(f"GAIN: {gain_for(20.0):.2f}  (afplay -v)")
    play_creak(gain_for(20.0))
    time.sleep(2.2)
    print("played creaking.mp3")
    return 0 if angle is not None else 1


def prove():
    """Play the creak at three levels so the user can pick a loudness."""
    vol, muted = audio_state()
    print(f"system output: volume={vol} muted={muted}")
    if muted or vol == 0:
        print("!! output is muted / at 0 — fixing that first")
        subprocess.run(["osascript", "-e", "set volume output muted false",
                        "-e", "set volume output volume 60"])
    if not os.path.exists(CREAK):
        print(f"missing sound file: {CREAK}")
        return 1
    for gain in (1.0, 1.5, 2.0):
        print(f"gain {gain:.1f}", flush=True)
        subprocess.run(["afplay", "-t", "2.2", "-v", f"{gain:.2f}", CREAK])
        time.sleep(0.4)
    print("done — peak of the sample is -6.2 dBFS, so 2.0 is the loudest clean setting")
    return 0


def watch():
    dev = None
    prev = prev_t = None
    while True:
        if dev is None:
            dev = open_lid_device()
            if dev is None:
                print("sensor unavailable (lid closed?) — retrying", flush=True)
                time.sleep(2)
                continue
        angle = read_angle(dev)
        now = time.time()
        if angle is None:
            dev = None
            continue
        speed = 0.0 if prev is None else abs(angle - prev) / max(now - prev_t, 1e-3)
        print(f"angle={angle:6.1f}  speed={speed:6.1f} deg/s  "
              f"{'TRIGGER' if speed >= SPEED_TRIGGER else ''}", flush=True)
        prev, prev_t = angle, now
        time.sleep(1.0 / POLL_HZ)


def main():
    if "--self-test" in sys.argv:
        return self_test()
    if "--prove" in sys.argv:
        return prove()
    if "--trigger-test" in sys.argv:
        return trigger_test()
    if VERBOSE:
        return watch()

    log(f"agent start v{VERSION} app={APP} trigger={SPEED_TRIGGER} deg/s")
    warned_silent = False
    dev = None
    prev = None
    prev_t = None
    last_play = 0.0
    closed_logged = False
    while True:
        if dev is None:
            dev = open_lid_device()
            if dev is None:
                if not closed_logged:
                    log("lid sensor unavailable (lid closed?) - retrying")
                    closed_logged = True
                time.sleep(2)
                continue
            log("lid sensor opened")
            closed_logged = False
            prev, prev_t = None, None
        angle = read_angle(dev)
        now = time.time()
        if angle is None:
            try:
                IOKit.IOHIDDeviceClose(dev, 0)
            except Exception:
                pass
            dev = None
            continue
        if prev is not None and prev_t is not None:
            dt = now - prev_t
            if dt <= SAMPLE_GAP_MAX:
                speed = abs(angle - prev) / dt
                play, reason = should_play(speed, now, last_play, creak_enabled())
                if play:
                    if not audio_ok() and not warned_silent:
                        warned_silent = True
                    vol = gain_for(speed)
                    play_creak(vol)
                    last_play = now
                if speed >= 1.5:
                    log(f"move {prev:.0f}->{angle:.0f} deg speed={speed:.1f} deg/s "
                        f"{'CREAK' if play else 'skip:' + reason}")
        prev, prev_t = angle, now
        time.sleep(1.0 / POLL_HZ)


if __name__ == "__main__":
    sys.exit(main() or 0)
install.sh · 3,552 B · 109 lines · sha256 21fd096883d8e42d…
#!/usr/bin/env bash
# Install lidcreak: makes FreeSlapMac's "Lid Creaking Sound" switch real.
# No root needed. Re-runnable (upgrades in place).
set -euo pipefail

BUNDLE="com.devesh.freeslapmac"
LABEL="${LIDCREAK_LABEL:-com.isaac.lidcreak}"
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEST="$HOME/Library/Application Support/FreeSlapLidCreak"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
LOGDIR="$HOME/Library/Logs"
APP="${FREESLAP_APP:-}"

say() { printf '\033[1m%s\033[0m\n' "$*"; }
ok()  { printf '  \033[32m✓\033[0m %s\n' "$*"; }
bad() { printf '  \033[31m✗\033[0m %s\n' "$*"; }

say "lidcreak installer"

# 1. FreeSlapMac present?
if [ -z "$APP" ]; then
  for c in "/Applications/FreeSlapMac.app" "$HOME/Applications/FreeSlapMac.app"; do
    [ -d "$c" ] && APP="$c" && break
  done
fi
if [ -z "$APP" ] || [ ! -d "$APP" ]; then
  bad "FreeSlapMac.app not found — install the free app first (github.com/ibedevesh/freeslapmac)."
  exit 1
fi
ok "app: $APP"

CREAK="$APP/Contents/Resources/Sounds/creaking.mp3"
if [ ! -f "$CREAK" ]; then
  bad "missing $CREAK — the sound asset ships with the app; reinstall FreeSlapMac."
  exit 1
fi
ok "sound: creaking.mp3 ($(wc -c <"$CREAK" | tr -d ' ') bytes)"

# 2. Python 3 (macOS ships one; Command Line Tools are enough)
if ! command -v python3 >/dev/null 2>&1; then
  bad "python3 not found — run: xcode-select --install"
  exit 1
fi
ok "python3: $(python3 -V 2>&1)"

# 3. Copy the helper out of the read-only download location
mkdir -p "$DEST" "$HOME/Library/LaunchAgents" "$LOGDIR"
install -m 0755 "$SRC/lidcreak.py" "$DEST/lidcreak.py"
ok "installed: $DEST/lidcreak.py"

# 4. LaunchAgent (user-level, starts at login, restarts if it dies)
launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 || true
cat > "$PLIST" <<PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>$LABEL</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/python3</string>
    <string>$DEST/lidcreak.py</string>
    <string>--app</string>
    <string>$APP</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>5</integer>
  <key>StandardOutPath</key><string>$LOGDIR/lidcreak.out.log</string>
  <key>StandardErrorPath</key><string>$LOGDIR/lidcreak.err.log</string>
</dict>
</plist>
PLISTEOF
plutil -lint "$PLIST" >/dev/null
ok "LaunchAgent: $PLIST"

# 5. Make sure the in-app switch reads ON (the app writes this pref itself)
defaults write "$BUNDLE" _creakingEnabled -bool true
ok "FreeSlapMac toggle '_creakingEnabled' = true"

# 6. Load it
if launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null; then
  ok "agent bootstrapped"
else
  launchctl unload "$PLIST" >/dev/null 2>&1 || true
  launchctl load -w "$PLIST"
  ok "agent loaded (legacy load -w)"
fi

sleep 1

# 7. Prove it: sensor read + one creak through the speakers
echo
say "self-test"
python3 "$DEST/lidcreak.py" --app "$APP" --self-test || {
  echo
  bad "the sensor did not answer. This Mac may not expose a lid-angle sensor"
  bad "(it exists on Apple Silicon MacBooks). The agent stays installed but idle."
  exit 2
}

echo
say "done"
cat <<EOF
  Tilt the screen slowly — you should hear the creak, and the log should show it:
      tail -f ~/Library/Logs/lidcreak.log
  Turn it off any time with FreeSlapMac's own "Lid Creaking Sound" switch,
  or remove everything with:  bash "$SRC/uninstall.sh"
EOF
uninstall.sh · 958 B · 25 lines · sha256 ac0154c64c475326…
#!/usr/bin/env bash
# Remove lidcreak completely. Leaves FreeSlapMac untouched.
set -uo pipefail

LABEL="${LIDCREAK_LABEL:-com.isaac.lidcreak}"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
DEST="$HOME/Library/Application Support/FreeSlapLidCreak"
UID_="$(id -u)"

printf 'Removing lidcreak…\n'
launchctl bootout "gui/$UID_/$LABEL" 2>/dev/null || true
launchctl unload "$PLIST" 2>/dev/null || true
pkill -f "FreeSlapLidCreak/lidcreak.py" 2>/dev/null || true

rm -f "$PLIST"
rm -rf "$DEST"
rm -f "$HOME/Library/Logs/lidcreak.log" "$HOME/Library/Logs/lidcreak.log.1" \
      "$HOME/Library/Logs/lidcreak.out.log" "$HOME/Library/Logs/lidcreak.err.log"

if launchctl list | grep -q "$LABEL"; then
  printf '  ! agent still listed — log out and back in, or run: launchctl bootout gui/%s/%s\n' "$UID_" "$LABEL"
else
  printf '  ✓ agent unloaded, files removed\n'
  printf "  FreeSlapMac's \"Lid Creaking Sound\" switch is dead again (it always was).\n"
fi
README.md · 6,936 B · 146 lines · sha256 e6947210b34db90e…
# lidcreak — the lid-creaking sound FreeSlapMac forgot to implement

FreeSlapMac (the free version of SlapMac) ships a **"Lid Creaking Sound"** switch that does
nothing. It isn't a permissions problem and it isn't your Mac: the free app's binary contains the
toggle, the description and the `_creakingEnabled` preference — and **no lid-sensor code at all**,
no `creaking.mp3` reference, and no wake/clamshell hooks. The switch is dead UI carried over from
the paid app, which does have a `LidAngleSensor` / `CreakAudioEngine` implementation.

The `creaking.mp3` asset is sitting right there in the bundle, unused.

This is a ~380-line user-level helper that reads the MacBook's hidden lid-angle sensor and plays
that unused asset while the lid moves. The app's own switch becomes the on/off control.

```
lidcreak/
├── lidcreak.py    the daemon (IOHID feature-report read + afplay)
├── install.sh     copies it, writes the LaunchAgent, self-tests it
├── uninstall.sh   removes every trace
├── README.md
└── LICENSE
```

## Requirements

| | |
|---|---|
| Mac | Apple Silicon MacBook (the lid-angle sensor is exposed on macOS 13+; verified on Mac14,2 / M2 Air, macOS 26.6) |
| App | FreeSlapMac installed in `/Applications` (or `~/Applications`) — provides `creaking.mp3` |
| Python | any `python3` (the one from Command Line Tools is enough) |
| Root | **none** — feature-report HID reads are unprivileged |

## Install

```bash
unzip freeslapmac-lidcreak-1.0.1.zip
cd freeslapmac-lidcreak
bash install.sh
```

It prints a step-by-step report and ends with a self-test that reads your real lid angle and plays
one creak. Nothing to approve, no login items to click, no System Settings trip.

If the self-test says `SYSTEM OUTPUT: volume=0 muted=True`, nothing on your Mac can be heard — not
this helper, not music, not anything. Unmute first:

```bash
osascript -e 'set volume output muted false' -e 'set volume output volume 60'
python3 "$HOME/Library/Application Support/FreeSlapLidCreak/lidcreak.py" --prove
```

`--prove` plays the creak three times at rising gain so you can hear exactly what you are
choosing between.

## Verify

```bash
tail -f ~/Library/Logs/lidcreak.log      # tilt the screen: move … CREAK / skip:slow
python3 "$HOME/Library/Application Support/FreeSlapLidCreak/lidcreak.py" --self-test
launchctl list | grep lidcreak           # agent is loaded and alive
```

A real session looks like this — 20 Hz sampling, one creak per movement burst:

```
20:31:03  move 80->85 deg speed=32.9 deg/s CREAK
20:31:03  creak vol=1.82
20:31:03  move 84->80 deg speed=39.6 deg/s skip:retrigger   (one creak per movement)
20:31:13  move 95->94 deg speed=4.6  deg/s skip:slow        (drift, ignored)
```

## Uninstall

```bash
bash uninstall.sh        # from the extracted folder
```

Removes the agent, the helper, and the logs. FreeSlapMac is not touched.

## How it works

The MacBook's lid angle is a HID sensor on **usage page `0x20`, usage `0x8A`**, read as
**feature report 1** (3 bytes: report ID, then the angle as uint16 little-endian — `01 68 00` = 104°).
`lidcreak.py` polls it at 20 Hz through `IOHIDManagerCreate` + `IOHIDDeviceGetReport`, computes
`Δangle / Δt`, and:

- fires when speed ≥ **8 °/s** (so slow drift and typing vibration stay silent),
- holds a **1.4 s** retrigger gap so one lid movement gives one creak,
- sets gain from speed: `clamp(1.0 + speed/40, 1.0, 2.0)` — `creaking.mp3` is mastered about
  30 dB below the tap sounds (mean −30.5 dBFS, peak −6.2 dBFS), so a "reasonable" 0.35 volume was
  inaudible even at full system volume. 2.0 is the loudest setting that does not clip,
- plays with `afplay -t 1.8` (truncated to the creak, not the whole 5.1 s file),
- reopens the device if the lid closes (the sensor disappears on sleep) instead of dying,
- obeys `_creakingEnabled` and nothing else: it creaks with FreeSlapMac open or quit, because the
  app was never the thing playing this sound (1.0.0 required the app to be running, which meant a
  closed app produced silence and no explanation),
- logs *why* a movement did not creak (`skip:slow`, `skip:toggle off`, `skip:retrigger`), and warns
  once when the system output is muted or at zero.

Tuning knobs (no reinstall needed, edit the `ProgramArguments`/constants or pass flags):

```bash
lidcreak.py --watch                 # live angle + speed, to pick your own threshold
lidcreak.py --trigger 5             # more sensitive
lidcreak.py --volume 1.5            # fixed gain (1.0-2.0) instead of speed-scaled
lidcreak.py --prove                 # three creaks, quiet to loud
lidcreak.py --trigger-test          # offline check of the trigger logic, no audio
```

## Honest limitations

- **Closing the lid will not creak.** macOS suspends the machine as the lid reaches ~10°, so the
  sensor is gone before the hinge gets there. The creak fires while the lid *moves* — the same
  behaviour the paid app has. This is physics, not a bug in the helper.
- **It is not a patch of the app.** The toggle in FreeSlapMac is still dead UI; this helper just
  reads it. Uninstalling the helper (or turning the toggle off) restores silence.
- **Audio goes through `afplay`**, i.e. routes wherever system output goes. There is no mixing
  into the app's own audio session — the app never had one for this.
- The sensor exists on Apple Silicon MacBooks; on Intel Macs and desktops `install.sh` says so and
  leaves nothing running.
- FreeSlapMac's slap detection itself is untouched. One quirk worth knowing: if several copies of
  the app's privileged wrapper are alive, a single slap can retrigger more than once. Quitting and
  reopening the app collapses them to one.

## License

MIT — see `LICENSE`. The `creaking.mp3` asset is **not** redistributed here: it is read from your
own FreeSlapMac installation. FreeSlapMac and SlapMac are the work of their respective authors;
this helper is an independent, unofficial companion.

Written by Isaac Chan, October 2026.

## Changelog

**1.0.1** — the version that actually makes a noise. Three faults, all found by reading the log
instead of trusting the code:

1. The daemon only played while FreeSlapMac was open. The app was never the thing playing the
   creak, so that gate only ever produced silence with the app quit — and the log said
   `trigger=no`, which reads like "your lid didn't move enough".
2. Gain. `0.35` on a file mastered at −30.5 dBFS mean is inaudible even at full system volume.
   Now `1.0–2.0`, measured against the file's −6.2 dBFS peak so it cannot clip.
3. No audio diagnostics. A muted Mac and a missing creak looked identical. `--self-test` now
   prints the output volume and mute state, the daemon warns once if the output is muted, and
   each skipped movement is tagged with its reason.

**1.0.0** — first release: HID sensor read, speed trigger, retrigger gap, LaunchAgent, self-test.
LICENSE · 1,369 B · 28 lines · sha256 101f1c881f5f9851…
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.

---

This package reads the creaking.mp3 asset out of a locally installed copy of
FreeSlapMac. That audio file is not part of this repository and is not
redistributed under this license. FreeSlapMac and SlapMac remain the property
of their respective authors; this is an unofficial companion utility.