Inspiration

I ran a small study with 49 high-school seniors. All students read the same SAT-style passage twice. The only difference between the two readings was the comment made by the teacher prior to the passage. The first time the teacher said, “just practice” and the second time the teacher said, “this one really matters.” Student test anxiety went from a 3.1 to a 4.0 and the reading score dropped from a 7.35 to a 6.78. This drop in score was significant (t(48) = 6.32, p < .001). Students completed the practice test first. This study was conducted in a classroom setting and not in a controlled experiment setting. However, this study gave me a very specific moment: the minute prior to a test.

For most students, this minute holds little. The average number of students serviced by a school counselor in the US is approximately 372 (ASCA, 2024-25). The closest service currently available is biofeedback. Biofeedback is typically performed in a supervised setting and costs $100 or more. There is a great deal of information available on test anxiety. However, there is also very little that can be done in ten minutes prior to a test, alone, with a cell phone.

So Cole and I created FATHOM: a breathing game disguised as a diving game.

What it does

You are a diver in a dark bioluminescent ocean. Your breath controls the controller (via the spacebar or microphone). Inhaling twice charges your dive light. You are counted through these breaths on a rhythm. The app does not try to detect them. Then a long slow exhalation, which is detected by the app, takes you deeper.

This is cyclic sighing. It was used in a one month randomised controlled trial (108 participants) by Balban et al. (2023, Cell Reports Medicine). Participants showed an improvement in mood and a reduction in breathing rate after five minutes a day of cyclic sighing compared to five minutes a day of mindfulness meditation. This shows the benefit of five minutes a day of cyclic sighing. Our game has not been tested in such a trial.

We designed it so that breathing well and diving well are one and the same. Depth increases significantly faster than the length of the exhaled breath. Almost three times as much depth is achieved with two six second exhaled breaths compared to twelve one second exhaled breaths. Breathing faster yields less benefit and holding your breath yields no benefit whatsoever. The level of difficulty is determined by your starting rate and will only ever ask you to decrease your rate. There is a minimum level that the prompt will not fall below.

Each dive lasts approximately five and a half minutes. Initially you will breathe normally for ten seconds to determine your starting rate. Then you will encounter three zones, each asking you to breathe at a slightly slower rate than the previous zone. Upon the surface you will be given your breathing rate before and after the dive, the difference between the two rates, and the time it took you to reach half way toward the slowest rate requested by the dive.

This rate is breaths per minute from the exhalation. It is a before-and-after measure for a single dive and is not a clinical measurement. It does not indicate the state of your health. Both the screen and the saved file indicate this. Then the app will instruct you to leave. There is no feed, no streaks, no chat, no account, and no server. There is a 90 second Quick dive available immediately before the assessment.

This is not for therapy, not for diagnosis, and not for use in a crisis. It is designed to train arousal regulation in real time. What is being regulated is your breathing rate. This is the only claim made by the app. Every time the app loads this is indicated on the initial screen prior to any request for microphone access. The following is also indicated: Avoid use while driving. If you begin to feel light-headed, stop using the app. If you have a heart or lung condition, are pregnant, or a history of fainting or seizures, consult with a clinician prior to regular use. Crisis resources (988 in the US, 9-8-8 in Canada, and findahelpline.com for over 130 countries) are available on this screen and the surface screen.

How we built it

Cole had everything that listens (src/breath and the worklet) and I had everything you see (src/game and src/shell). There was only one short file of event types written in TypeScript between us.

Cole’s engine: The microphone gets boiled down to a few bands and sampled 50 times a second in an AudioWorklet. These are then run through a detector that adapts to the noise floor and outputs the start and duration of an exhale based on steadiness and length (not loudness). This means you can’t win by blowing harder. The calibration requires at least ten seconds and three exhales before outputting a baseline or failing instead of guessing. After that the current speed is computed as the median interval between the starts of exhales. If the room becomes too loud the engine will indicate that as well. No sound is recorded or uploaded. The published app is a static file folder hosted on Render.

I contributed to all that you can see in half. There's the PixiJS implementation of the dive scene, the session state machine and the "conductor" that handles the timing of exhalations to the engine (when they are requested and for how long). Also the scope screen, the surface readout, the export function for dive logs, the French text and spacebar mode. The codebase uses TypeScript and React 19 on Vite. There are 80 tests written between us that run directly using plain Node (no build required). They target areas where being incorrect would be silent: descent scoring, rate estimation, prompting timing and the only-slower rule.

Challenges we ran into

Inhales produce broadband noise and due to the limitations in our existing features (band levels rather than spectrogram) we cannot reliably distinguish an inhale from an exhale. During our first attempt at recording a user performing cyclic sighing, approximately 65% of the number of inhales counted resulted in an overestimation of the rate. As a result we decided to stop deceiving ourselves. Instead, the app will trigger an inhale according to a rhythm and will only listen for the subsequent exhale. An exhale will only be counted if it was initiated during the time period when an inhale was requested. This is indicated on the scope screen, the surface screen and in the exported file.

Rooms: A quiet room and a school hallway present very different conditions to the microphone. The noise-floor gate and the early room check were both discovered through observing failures during actual sessions. The room check exists to ensure that users are not breathing into a device that cannot detect their breathing.

Keeping the game honest. Points, streaks, a leaderboard: all would have given something other than a slow breath. None did.

Accomplishments that we're proud of

The reward loop consists of only one file. There are three rules that can be read directly from it: longer is better than shorter, slow is better than fast, and holding your breath gets you nothing. The difficulty can only slow the target. This check exists in the code that controls the rhythm. There is a test that exercises this code to make it go fast and expects it to refuse. The number at the end is based on your breathing and not selected from a mood slider.

What we learned

Smaller was better in the minute prior to testing. Each feature we removed improved the experience. A limit must be visible to users in order to be honest. "the inhale is not measured" is visible on screen and in the saved file, not just in the README. Once all the work is in GitHub issues and there is a one-types file between us, we barely need to communicate with each other.

What's next for Fathom

A small pilot in a counselor's office. The before/after values come from actual pre-testing time points instead of our own. Comparing the rate estimate to a chest strap. More languages. And nothing that tries to bring you back every day. It is designed for a specific ten minutes.

Built With

Share this project:

Updates

Submission history