Skip to content

Improve microphone routing and app guidance - #1

Draft
antonynjoro wants to merge 7 commits into
mainfrom
dev
Draft

antonynjoro wants to merge 7 commits into
mainfrom
dev

Conversation

@antonynjoro

Copy link
Copy Markdown
Owner

Summary

  • add explicit microphone discovery and selection in the menu bar
  • prefer the MacBook built-in microphone unless an external input is chosen
  • rebuild stale audio capture sessions after calls or device-route changes
  • extend the stuck-recording safety timeout
  • refresh the project README

Why

A macOS call could leave the long-lived audio engine with an invalid input route, producing a "No microphone available" error. ZeroG also followed the system default input, so connecting a Bluetooth headset could unexpectedly select its microphone and degrade playback quality.

Impact

ZeroG now defaults to the built-in Mac microphone, remembers explicit external choices, falls back safely when a selected device disappears, and retries a stale route with a fresh AVAudioEngine capture session.

Validation

  • swift test: 96 tests passed
  • build_app.sh: packaged ZeroG.app successfully
  • installed app tested locally with the built-in microphone and Sony WH-1000XM4 output

antonynjoro and others added 7 commits July 3, 2026 15:28
Two field bugs, one root: missed/misread trigger-key release.

- Fn/Globe: macOS swallows the key-up (globe action), the listen-only
  tap never sees the release, and the app stays stuck in .recording —
  the next tap then pastes the previous recording's audio.
- Right Shift: CGEventSource.keyState() misreports held modifiers
  (reads "up" mid-hold), so the earlier watchdog draft force-stopped
  recording after its first poll.

The watchdog now polls CGEventSource.flagsState() — the session's live
modifier flags, the same domain as the flagsChanged events the tap
consumes. Each TriggerKey carries its family mask (maskShift,
maskSecondaryFn, ...) for that check. Held key keeps its flag set (no
false stop); a swallowed release still clears the flag, so the watchdog
ends the recording within ~0.12s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stuck-recording report turned out flaky and the release build is
unobservable: every pipeline breadcrumb was Log.debug, compiled out of
release, so 'log show' had nothing. Three changes:

- Log.info: always-on NSLog breadcrumbs (unified log) for press/release/
  watchdog/timeout, audio + inference durations, and paste outcome.
  Transcript content stays debug-only; release logs lengths, not text.
- ParakeetTranscriptionEngine.transcribe now runs under a 30s hard
  deadline. A hung ANE call surfaced as indefinite .processing — the UI
  sat on 'transcribing' forever and silently dropped the next press.
  Now it errors and the state machine recovers.
- A trigger press while still .processing plays the Basso error sound.
  Previously the press was swallowed silently, the user spoke into a
  dead mic, and the late-arriving previous transcription pasted 'on its
  own' — the exact reported symptom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd on all not-ready presses

Field capture 2026-07-03 15:44 confirmed two gaps:

- NSLog dynamic content is redacted to <private> in the unified log even
  with %{public}@ — that specifier is honored by os_log only. Log.info/
  Log.error now use os.Logger with privacy: .public interpolation, so
  'log show --predicate' finally yields readable breadcrumbs from a
  release build.
- The captured incident was a press swallowed ~40s after relaunch while
  the model was still loading — a state the Basso feedback didn't cover
  (it was gated to .processing). The busy sound now plays for every
  not-ready press, so a dead-feeling hotkey is always audible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field-captured root cause of the flaky stuck-recording / stale-paste
bug (breadcrumbs, 2026-07-03 15:53): resetToIdle schedules an
unconditional success → idle transition ~2s after a paste. Pressing the
trigger again inside that window went success → recording, then the
stale timer fired and forced recording → idle. On release,
beginProcessing's 'are we recording?' state check failed, so the mic
was never stopped — the session kept capturing and merged into the next
tap's audio, which then pasted both utterances late ('pastes what I
said earlier'). Reproduced only when the next press came within ~2s of
the previous paste, hence the flakiness.

- resetToIdle now captures the state it was scheduled from and no-ops
  if the state has moved on by the time the timer fires.
- beginProcessing gates on the recorder's own isRecording instead of
  the display state, so a missed stop can't recur even if some future
  transition knocks the state machine out of .recording mid-capture.
- Regression test: resetToIdle must not stomp a recording that started
  inside the delay window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Settings

Clicking the Accessibility step's button fired the AX prompt and opened
the Settings pane in the same breath. The pane landed on top (hiding
the OS 'ZeroG would like to…' dialog) and could render before the
prompt's pre-listing registered — users arrived at an Accessibility
list with no ZeroG row to toggle.

First click now fires only the native prompt; its own Open System
Settings button lands on a pane where ZeroG is already listed. macOS
shows that prompt at most once per app record, so subsequent clicks
fall through to the existing request-then-deep-link path, with the
button retitled accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant