Inspiration

What it does

How we built it

Challenges we ran into

Accomplishments that we're proud of

Inspiration

Some men genuinely hate the sound of their own voice. They don't send voice notes. They get called "ma'am" on the phone. It's one of the few things about how you come across that nobody hands you a plan for — a speech-language pathologist can fix it, but that's $120 an hour and a waitlist, so almost nobody goes.

So I read the category before writing a line of code. Every direct competitor was tiny or abandoned; the biggest deep-voice app in the App Store had 145 ratings. The Spanish storefront had nothing — searching "voz grave" returns bass boosters. And four complaints repeated across the reviews of the apps that did exist:

  • an "AI voice score" that moved when nothing had changed
  • a pile of exercises with no order to do them in
  • month-miracle promises, then silence
  • subscriptions you couldn't cancel

Every one of those is a trust problem, not a feature gap. That's what I built against.

What it does

Basso measures your speaking pitch in hertz, builds a week-by-week program from your own baseline, and re-tests you on the same test every week so you watch the number move. Ten minutes a day, six to eight weeks, and it says so on the store page rather than promising a month.

Your voice never leaves the phone. There's no account, no server, no upload. In most apps that's a compliance line; here it's the sentence that gets someone to press record in the first place.

How I built it

Flutter, one codebase, iOS and Android. Riverpod, drift, go_router, RevenueCat for subscriptions, Firebase for crashes and counters. No backend — there's nothing to run.

The pitch engine is pure Dart, no native plugin. Per frame (2048 samples at 44.1 kHz, ≈46 ms, hop 512 ≈ 11.6 ms): DC removal, a one-pole 40 Hz high-pass, RMS in dB, then YIN. The difference function

$$d_t(\tau) = \sum_{j=1}^{W}\left(x_j - x_{j+\tau}\right)^2$$

is normalised cumulatively so the trivial $\tau = 0$ minimum stops winning:

$$d't(\tau) = \begin{cases}1 & \tau = 0\[4pt] d_t(\tau) \Big/ \left[\tfrac{1}{\tau}\sum{j=1}^{\tau} d_t(j)\right] & \tau > 0\end{cases}$$

Basso takes the first dip below $0.15$, descends to the local minimum, and refines it by parabolic interpolation, giving $f_0 = f_s/\tau$ over 50–400 Hz. A frame counts as voiced only when RMS beats the pre-roll noise floor by 10 dB and aperiodicity is under $0.2$; vocal fry is rejected below 65 Hz or on a jump over 15% from the previous voiced frame. Full-clip analysis is pure and synchronous, so it runs in Isolate.run and never touches a frame of UI.

Reporting hertz is honest but incomplete, because pitch is perceived logarithmically:

$$\Delta n = 12\log_2!\left(\frac{f_1}{f_0}\right)$$

A 15 Hz drop is $-1.65$ semitones from 165 Hz but $-2.31$ from 120 Hz — the same number on the screen, a noticeably different change in the ear. So the user sees Hz, which is concrete, while the program's targets are relative.

Around all of that: 1,129 tests and a gate that must pass before anything ships — codegen, l10n, format, analyze, test.

Challenges I ran into

A crash fix that destroyed the evidence. Crashlytics pointed at a RangeError in the pitch engine's RMS calculation. I bounded the range, the crash stopped, and I moved on. But the cause — an Android mic stream that closes itself about 40 ms in on some devices — was still there. Worse, the crash had been the only proof those devices existed. Now the clip just fell quietly into the quality gate and told the user "we couldn't hear you." It came back two weeks later as field complaints. Silencing a symptom can delete the diagnosis; if you must silence one, leave telemetry behind.

A build that only failed in review. Three releases shipped with the wrong entrypoint, which meant they carried the dev flavor's RevenueCat test-store key — and the iOS SDK deliberately traps on a test key in a release binary. The app died ~300 ms after launch, on Apple's iPad, and nowhere else. The build command was silently wrong; only the artifact knew. Releases now go through a script that greps the binary it just produced and refuses to ship one carrying a test key or an ad-ID permission the privacy declaration denies.

A paywall with no honest exit. My gate was one line: access requires entitlement. Meanwhile both store descriptions promised a free first week. A reviewer dismissed the paywall, landed on a home screen that could do nothing, and read it as a trap — correctly. The fix wasn't wording, it was sequencing: week one is now genuinely free, in full, because this product's value is a number that moved, and someone who's never seen his own baseline has nothing to buy yet.

Pricing that punished the people who believed me. I launched $6.99/week and $39.99/year. Basso's honest claim is six to eight weeks — so a user who took that literally and paid weekly spent $8 \times 6.99 = \$55.92$ to finish something the annual sold for $\$39.99$. The ladder contradicted the promise. Three tiers now save at every step, and product IDs carry no price, so repricing never orphans a SKU.

One missing character. Google Play rejected the app: the privacy policy URL "does not link to a valid privacy policy page." The page was fine. But nginx sits behind a TLS-terminating proxy, so $scheme inside it is always http, and its trailing-slash redirect was absolute — an HTTPS request came back pointed at http://. Browsers walk that chain silently, which is exactly why every check I'd run by hand had passed. An automated checker doesn't.

What I learned

A claim lives in more than one place. "The first week is free" existed in the code, in two store descriptions, and in the review notes — and they disagreed. Fixing one copy isn't fixing the claim, and the disagreement itself is the defect.

"It works in my browser" isn't verification. Browsers are built to forgive: they follow sloppy redirects, retry, guess. Reviewers' tooling doesn't. That one taught me to test the way the machine tests, not the way I browse.

Monetization is a product claim. If the copy says eight weeks, the pricing has to make eight weeks the cheapest honest path. Otherwise the paywall is arguing with the marketing, and the user notices.

What's next

iPad, deliberately held back until the phone build was stable. A voice-masculinization track — same engine, different program, an audience currently paying clinicians for it. And reminders tuned around the weekly re-test, because the re-test is the moment that makes someone come back.

What we learned

What's next for Basso: Deep Voice Training

Built With

Share this project:

Updates

Submission history