FreeSlapMac 1.1.0 ships a Lid Creaking Sound toggle: the label, the
description, the _creakingEnabled preference in its binary, and a 267-clip sound
pack that includes creaking.mp3. What it does not ship is a single line that reads
the lid-angle sensor. This is the native app that closes the gap — it reads the sensor 20 times
a second, decides whether the lid actually moved, and plays that unused clip. Then it turned out
the decision was the hard part: my first sensitivity setting fired on a one-degree
movement, and the reason is arithmetic.
First launch needs right-click → Open: the bundle is ad-hoc signed, not
notarised. It reads the creak sample out of your own /Applications/FreeSlapMac.app — the audio
belongs to that app's author, so it is not redistributed here.
The evidence is all inside FreeSlapMac's own bundle, and it took reading the binary rather than the UI to see it:
| what a working feature would need | what FreeSlapMac 1.1.0 has |
|---|---|
| a UI control | ✔ “Lid Creaking Sound” toggle + description text |
| a stored preference | ✔ _creakingEnabled in com.devesh.freeslapmac |
| a sound asset | ✔ creaking.mp3, one of 267 clips in the pack |
| code that reads the lid sensor | ✘ none — no HID usage page 0x20, no 0x8A |
| code that references the asset | ✘ zero references to creaking.mp3 |
So the toggle writes a preference nothing reads, and the sound sits in the bundle unused. That is what “it doesn't work” meant: not broken, absent. The rest of this page is the app that supplies the missing half, and then the part that took three attempts — deciding when a movement is a creak.
The MacBook lid angle is an HID feature report any user process can read — no root, no kernel extension, no private framework:
uint16 little-endian, degrees
resolution1° — whole degrees, no fractions
sampled20 Hz (50 ms), the app's own loop
drops outwhen the lid closes the device disappears; re-enumerated on the next sample
That third row — whole degrees — is the whole bug in the next section. A sensor that reports integers cannot tell you how fast something moved inside one sample; it can only tell you that the number changed by at least 1.
The first version measured speed the obvious way: the difference between two consecutive samples, divided by the sample interval.
speed = |angle[n] − angle[n−1]| / 0.05 s
one quantisation step: 1° / 0.05 s = 20 °/s
thresholds shipped in 2.1.0: Low 15 · Normal 8 · High 5 °/s
Twenty degrees per second is above all three. The least sensitive setting was already more sensitive than the smallest movement the hardware can express, so every setting fired on a 1° twitch — and a hand resting on a laptop produces exactly that, constantly. The user's report was precise: “the sensitivity didnt matter i did low and i angle changed 1 degree still creaked”. It wasn't a slider that needed different numbers. It was the wrong quantity being measured.
--selftest now exits 2 and prints exactly that.
Three gates, each answering a different question, and none of them trusting a single sample:
With the measurement fixed, the thresholds could finally mean something, and they were rescaled to the new numbers: Low 35 · Normal 22 · High 14 °/s. Then a 1.4 s retrigger cooldown, so a slow drag creaks once rather than machine-gunning.
Two constants exist purely to survive sensor noise, and are worth naming because they are the kind of thing that looks like a magic number until you know the failure: a span shorter than 0.18 s is not trusted for travel, and a gap larger than 0.5 s between samples (sleep, stall, app suspended) is discarded rather than divided into a fake velocity. A jump of more than 40° in one step is treated as the lid closing, not as speed.
Testing this by hand is hopeless — you cannot move a lid by exactly one degree on purpose, and you
cannot reproduce the movement later. So the decision logic is a pure Detector that takes
(time, angle) samples, and the sensor is just one thing that feeds it. --simulate
drives eight scripted timelines through the same object the app uses at runtime:
$ "Lid Creak.app/Contents/MacOS/LidCreak" --simulate
1° blip then held still fires=0 peak=6.7 °/s PASS
1° every 300 ms (slow creep) fires=0 peak=3.3 °/s PASS
12 °/s opening, High (needs 14) fires=0 peak=12.0 °/s PASS
3° tap in 150 ms, High (a tap, not a slide) fires=0 peak=20.0 °/s PASS
16 °/s sustained 2 s, High (gentle slide) fires=2 peak=16.0 °/s PASS
22 °/s opening, Normal fires=1 peak=22.0 °/s PASS
80 °/s slam, Low fires=1 peak=80.0 °/s PASS
20 °/s opening, Low (needs 35) fires=0 peak=20.0 °/s PASS
SIMULATE PASS
Read the first row against the old bug: the exact movement that used to fire at every setting now sits at 6.7 °/s — below the strictest threshold (High, 14) — and fails the travel gate before speed is even considered. Row four is the other half of the fix: a 3° tap genuinely moves at 20 °/s, and still doesn't creak at High, because a tap is not a slide.
--record exists for the opposite direction: it logs a real sensor trace so a movement that
did creak can be replayed offline instead of argued about.
build.sh runs these before it will call the build finished — every one sits on a boundary
where an off-by-one hides:
1° travel @ 20 °/s, High (the old bug) -> no PASS
2.9° travel @ 80 °/s, High -> no PASS
3.5° travel @ 45 °/s, Low -> yes PASS
3.5° travel @ 20 °/s, Normal (too slow) -> no PASS
3.5° travel @ 25 °/s, Normal -> yes PASS
12° travel @ 80 °/s, Low -> yes PASS
8° travel @ 12 °/s, High (gentle, too slow) -> no PASS
8° travel @ 15 °/s, High -> yes PASS
3.5° travel @ 45 °/s, disabled -> no PASS
3.5° travel @ 45 °/s, 0.5 s since last -> no PASS
10/10 PASS
Row two is the travel gate on its own: 80 °/s is a fast flop, but 2.9° of total movement means it never happened. Row eight is the retrigger cooldown doing its job.
The requirement was a creak you can hear, not a silenced app. The sample comes from FreeSlapMac's own pack; it is never modified in place:
creaking.mp3 in FreeSlapMac.app, FreeSlapMac Silent.app,
SlapMac.app or slapmac.app.~/Library/Application Support/LidCreak/creak.caf, once,
normalised to −1 dBFS. The gain is measured from the clip's own peak, not hardcoded — the source
peaks around 0.11 (≈ −19 dBFS), roughly a ×8 lift. Re-running is idempotent.0.55 + speed / 120, clamped to 1.0 — a slow drag is quiet,
a fast flop is full.If none of those apps is installed the app still runs and still reads the sensor, but the menu item reads Test creak (no sound file) and the log says so, rather than failing silently. That honesty is the direct lesson from the 422 silent triggers.
1. unzip → Lid Creak.app
2. drag it to /Applications
3. right-click → Open (ad-hoc signed, not notarised — a plain double-click is refused)
or: xattr -dr com.apple.quarantine "/Applications/Lid Creak.app"
open "/Applications/Lid Creak.app"
Then: a window with the live angle, the measured speed and a switch. Settings live in
defaults under com.isaac.lidcreak, so they survive a restart and can be scripted
— defaults write com.isaac.lidcreak enabled -bool false turns it off from the terminal.
--simulate PASS