Build

After speech recognition, my alarm would not start ringing again

After speech recognition, resume() reported success while the speaker stayed silent. RYZA now rebuilds the player and checks that sound came back.

The alarm was still on screen. The retry button was there. The app was telling the user to try again.

The room was quiet.

I found this while testing a voice mission that needs three correct readings. I read the first sentence, watched the progress update, and waited for the alarm to keep ringing until the next attempt. The sound stopped instead.

Nothing crashed. Speech recognition had finished cleanly. The mission was still open. From a screenshot the whole flow looked correct, which is what made it unpleasant — the interface was telling the truth only halfway. The mission was still active. It no longer felt like an active alarm.

For a form or a quiz, silence between attempts is fine. For an alarm, silence is the failure state. If someone can put the phone down after one imperfect attempt and fall asleep again, the voice mission is doing less than the plain alarm it replaced.

Pausing the sound is not the bug

The pause is deliberate and it has to stay. Speech recognition works badly when the phone’s own alarm is playing through the speaker, and I did not want the recognizer transcribing the alarm, or the user shouting over it.

So one voice attempt looks like this:

Alarm rings

User taps "Start reading"

Alarm audio pauses

Speech recognition listens

App checks the spoken sentence

Alarm audio returns if the mission is not complete

The last line was where it fell apart, and there were more ways to arrive there than I had counted on. A wrong sentence. A recognizer timeout. The speech service being briefly unavailable. One correct reading out of the three the mission wanted. An interruption while the microphone was still open.

I had treated those as five separate outcomes, because on screen they are. Wrong answer shows a retry prompt, one-of-three shows progress, no microphone shows a fallback message. The audio was handled inside each of those branches instead of above them, so one path could remember to bring the sound back while its neighbour forgot.

That is how an alarm goes quiet without ever being dismissed.

resume() returned success and the speaker stayed silent

My first fix was to call the audio player’s resume() when the app went back to the retry state. In code it read exactly like the thing I wanted: pause, listen, resume.

It did not hold up.

After speech recognition stops, the audio sessioniOS’s shared setting for what an app is currently allowed to do with the speaker and the microphone did not always come back in the state the player expected. Sometimes the player still believed it was paused. Sometimes it had effectively finished. Sometimes its own state said one thing while nothing came out of the speaker.

A successful function call was not proof that the alarm was audible. I had built the check around an implementation detail — did the player accept a resume command — when the only question that mattered was whether the room was loud again.

This is the same shape as a bug I hit in exact speech matching: the code was answering a question next to the one the user was asking.

One rule instead of five outcomes

I moved the audio decision out of the individual branches and gave it to the mission controller, as a single rule.

Unless the mission has genuinely finished, failure or interruption returns to an audible alarm.

Every outcome now resolves through the same rule, and the prompt on screen is the only part that differs:

What happenedWhat the user seesAlarm audio
Wrong readingRetry promptOn
Speech timeoutRetry promptOn
Speech service unavailableFallback promptOn
One correct reading of threeProgress promptOn
Microphone interruptedRetry promptOn

The fourth row is the one I argued with myself about. A user who reads the first sentence perfectly and still owes two more has done something right, and restarting the alarm on top of a progress message felt like a punishment. I left the sound off there at first because it was friendlier.

That was the wrong call. The mission exists because the wake-up task is not finished, and a friendly silent gap is a way to abandon it while still half asleep. The progress message was worth keeping. It was not a reason to weaken the alarm.

Restarting beat resuming

I stopped treating the player as a reliable paused object once a microphone session had been through it.

When the alarm needs to come back now, the app throws the old player away and starts fresh: new player, speaker route and audio focus configured again, loop enabled, the selected alarm asset played from the beginning. Vibration restarts in the same step, so sound and buzz cannot drift into separate states.

It is wasteful, and I would rather have the certainty than a playback position nobody wanted.

Old assumption:  the player was paused, so resume it.
New assumption:  the voice attempt changed the audio environment,
                 so start the alarm again from a known state.

Three tries, because a restart can lose the race too

Even a fresh playback request can arrive while the microphone session is still shutting down, so the app asks up to three times and checks its work in between.

voice attempt ends start playback immediately is it actually playing? YES done NO retry at 350, 900 ms
Immediately, then 350 milliseconds later, then 900 milliseconds later. Each attempt waits a moment and then asks the player whether sound is actually coming out.

The retries are short on purpose. The goal is not to keep hammering at it in the background, it is to cover the moment where iOS has accepted the microphone shutdown but has not made the speaker path ready yet. If it has not worked by 900 milliseconds, another attempt at four seconds would arrive long after the user has decided the app is broken.

There is also a light audio health check every five seconds while a mission is active. It stays out of the way while someone is speaking. It exists for the stranger case: the app believes it prepared the mission, the screen is still open, and the sound has quietly gone after an interruption. When that happens the app records a diagnostic event and tries to restore the alarm.

The tests I did not have before

The cases worth writing down are the ones that used to slip through, so the tests are all on the awkward paths rather than the happy one.

A failed reading returns to the retry screen with audio on. A correct reading that does not complete the mission also restores audio. A timeout restores audio. A microphone interruption does not count as a failed reading, but the alarm becomes audible again. A prepared session that ends up silent is caught by the health check.

One test guards the opposite mistake: the Wake Sequence flow must not restart audio while the user is in the middle of its puzzle. A loud alarm firing back up during a deliberate interaction is its own kind of broken. The rule was never “always play audio”. It is “do not leave an unfinished alarm silently inactive”, and the difference between those two sentences is most of this work.

What a user gets out of it

Nothing they would notice, which is the point.

The alarm pauses while the app listens. If the mission is unfinished for any reason at all, the sound comes back. If the first attempt was perfect but two readings remain, the sound still comes back.

I have stopped judging alarm flows by whether the success path works. The success path is usually the easy one. What the product is actually tested by is a half-correct reading, a slow microphone, an interruption at the wrong second, and someone too tired to work out why the phone went quiet. This is the third time the system layer and the app layer have disagreed about state, after an alarm that was rescheduled while it was still ringing and a reboot leaving the app unable to read its own data.

An alarm that stays audible between voice attempts is not a feature anyone will thank me for. It is the difference between a wake-up task and a very polite suggestion.