Our AirPods know more about us than we think. They have an accelerometer, a gyroscope, and they know exactly how your head is oriented in space — updated dozens of times per second, all day long.
Apple uses that data for spatial audio and ear detection. But the raw signal is sitting right there, accessible to any developer through Core Motion. Most people have no idea. I got curious about what else you could do with it, so I spent a weekend finding out.
What I Built
The app sits quietly in your macOS menu bar and uses your AirPods' motion sensors to monitor your posture in real time. Green when you're upright, red when you're not. Slouch for more than ten seconds and it notifies you.
No new hardware, no wearable — just sensors you're already wearing.
The API
The class is CMHeadphoneMotionManager, part of Core Motion. Apple added it in iOS
14 and macOS 14. You call startDeviceMotionUpdates and it streams CMDeviceMotion
objects at around 50 Hz — attitude, gravity, rotation rate, user acceleration, the
full suite you'd normally get from an iPhone.
One gotcha nobody warns you about: if you call startDeviceMotionUpdates once at
launch and the AirPods aren't connected yet, the stream just never starts. No retry,
no error — it silently does nothing. I had to implement
CMHeadphoneMotionManagerDelegate to listen for the connect event and restart the
stream every time headphones are detected.
How Posture Detection Works
I use CMDeviceMotion.attitude.pitch, which measures forward-and-backward head tilt
in radians. The key constraint is you can't hardcode a baseline — everyone holds
their head differently, and AirPods sit differently in every ear. So there's a
calibration step: press Calibrate, and the app samples your head pitch continuously
for two seconds and averages the readings. That average becomes your personal
baseline for "upright," stored in UserDefaults so it persists across launches.
From there, every frame:
private func updateScore(pitch: Double) {
guard let base = baselinePitch else { return }
let deviation = abs(pitch - base)
let maxDeviation = 0.35 // roughly 20 degrees
postureScore = max(0, min(100, 100.0 * (1.0 - deviation / maxDeviation)))
checkBadPosture()
}0.35 radians is about 20 degrees — at that deviation the score hits zero. In practice most people get a warning well before that.
The Ten-Second Rule
Slouch detection doesn't fire the moment your score drops. It starts a timer the first time you cross below 50%. Correct yourself before ten seconds and the timer resets. Stay slouched for ten continuous seconds and it fires a notification — then won't send another for sixty seconds even if you're still slouched, so it can't spam you.
private func checkBadPosture() {
if postureScore < 50 {
if badPostureStart == nil {
badPostureStart = Date()
} else if let start = badPostureStart,
Date().timeIntervalSince(start) > 10 {
sendNotificationIfNeeded()
}
} else {
badPostureStart = nil
}
}
private func sendNotificationIfNeeded() {
let now = Date()
if let last = lastNotificationTime,
now.timeIntervalSince(last) < 60 { return }
lastNotificationTime = now
// send UNUserNotificationCenter request
}The Bug That Cost Me Three Hours
My first version computed deviation as a full 3D angle between gravity vectors — dot
product, acos, the works. It looked more correct on paper. In practice it was
terrible, because the gravity vector changes when you turn your head left or right,
not just when you lean forward. Glancing at a second monitor would tank the posture
score even with a perfectly straight spine.
Going back to plain attitude.pitch fixed it immediately. Pitch only changes on the
axis that matters for posture — forward and backward tilt. Turning your head doesn't
touch it.
What Surprised Me
While staring at the raw motion data sitting completely still, I noticed a tiny rhythmic oscillation in the signal. Not noise — breathing. Every inhale and exhale moves your head by a fraction of a millimeter, invisible to the eye, completely visible to the accelerometer.
Add a stillness gate, a detrend pass, and peak detection, and you have a live breathing-rate monitor falling out of the exact same data stream. That feature took three hours.
The Bigger Point
We're walking around with sophisticated sensor arrays in our ears, on our wrists, in our pockets. Most apps treat them as input devices — tap here, swipe there. But the passive data these sensors produce continuously is mostly untouched: head orientation, micro-movements, breathing patterns, gait. All of it is there, all of it is accessible, almost none of it is being used in interesting ways.
The next wave of health and wellness software might not need new hardware at all. It just needs developers willing to look at what the sensors already on us are telling us.