Skip to content

fix(ios): keep tick/chime cadence synchronized with timer#207

Merged
auerbachb merged 3 commits into
mainfrom
issue-201-ios-chime-sync
Apr 22, 2026
Merged

fix(ios): keep tick/chime cadence synchronized with timer#207
auerbachb merged 3 commits into
mainfrom
issue-201-ios-chime-sync

Conversation

@auerbachb
Copy link
Copy Markdown
Owner

@auerbachb auerbachb commented Apr 22, 2026

Summary

  • refactor AudioEngine to keep a persistent warm AVAudioEngine alive instead of creating a new engine for each tick/chime playback
  • add iOS lifecycle + interruption handling so audio only resumes when appropriate and can recover after engine configuration changes
  • warm up the audio engine when sessions start/resume so first tick latency does not skew perceived cadence
  • ignore ios/StillPointShared/.build/ artifacts to keep local SwiftPM outputs out of review/PR noise

Closes #201

Test plan

  • swift test --package-path ios/StillPointShared
  • Start an iOS session and verify tick cadence stays aligned with the visible second counter through a full run
  • Pause/resume an iOS session several times and verify tick/chime resumes on the correct cadence without drift
  • Background/foreground during an active iOS session and verify audio cues remain aligned with timer progression afterward
  • coderabbit review --prompt-only (clean pass)
  • coderabbit review --prompt-only (second clean pass)

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced audio reliability during app interruptions and background transitions.
    • Improved timing accuracy for audio feedback sounds.
  • Chores

    • Updated build artifact exclusions in version control.

Use a persistent warm AVAudioEngine with interruption and lifecycle resume handling so tick/chime playback timing tracks timer progression across start, pause/resume, and app state transitions.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
still-point Ignored Ignored Preview Apr 22, 2026 11:23pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@auerbachb has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 3 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba746bd8-5966-49b4-b8ea-997d3d310960

📥 Commits

Reviewing files that changed from the base of the PR and between 28168d5 and ecf2b17.

📒 Files selected for processing (2)
  • ios/StillPointApp/ViewModels/SessionViewModel.swift
  • ios/StillPointShared/Sources/StillPointShared/AudioEngine.swift
📝 Walkthrough

Walkthrough

This PR adds iOS audio warm-up functionality to synchronize audio cues with timer display progression. Changes include a persistent AVAudioEngine with lifecycle management, sample rate synchronization for audio callbacks, SessionViewModel integration of warm-up initialization, and .gitignore updates for build artifacts.

Changes

Cohort / File(s) Summary
Audio Engine Core Implementation
ios/StillPointShared/Sources/StillPointShared/AudioEngine.swift
Introduces warmUp() method and persistent AVAudioEngine lifecycle. Implements AVAudioSession interruption handling, app background/foreground transitions, and engine configuration observers. Refactors audio rendering to derive active sample rate from engine mixer output and pass it to generator callbacks. Removes per-sound engine creation; now reuses persistent engine with temporary node connections.
Session Management Integration
ios/StillPointApp/ViewModels/SessionViewModel.swift
Adds warmUp() call immediately after startDate update in start() method. Updates resume() to prevent duplicate start() invocation when view model is not paused.
Build Configuration
.gitignore
Extends exclusions to ignore ios/StillPointShared/.build/ directory alongside existing build/ pattern.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Lifecycle
    participant SVM as SessionViewModel
    participant AE as AudioEngine
    participant AVS as AVAudioSession
    participant Engine as AVAudioEngine
    
    App->>SVM: start()
    SVM->>SVM: update startDate
    SVM->>AE: warmUp()
    AE->>AVS: configure session
    AE->>Engine: ensureEngineRunning()
    Engine->>Engine: start on serial queue
    AE->>AE: register lifecycle observers<br/>(interruptions, background/foreground)
    AE-->>SVM: warmUp() complete
    SVM->>SVM: start timer/scheduling
    
    Note over AE,Engine: Later: Audio Playback
    SVM->>AE: playSynthesized()
    AE->>Engine: query mixer sampleRate
    AE->>AE: generator callback<br/>with active sampleRate
    AE->>Engine: connect source node
    Engine->>Engine: start (if needed)
    Engine-->>AE: render audio frames
    AE->>Engine: disconnect/detach source
    
    App->>AE: background transition
    AE->>Engine: pause conditionally
    App->>AE: foreground transition
    AE->>Engine: resume conditionally
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hoppy whiskers twitch with glee,
The audio drifts no more, you see!
With engines warmed and samples true,
Ticks and chimes stay in time—kazoo!
No more async hops, the beat's aligned,
A rhythmic warren, perfectly timed! 🎵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: fixing audio tick/chime synchronization with the timer on iOS, which directly matches the PR's main objective.
Linked Issues check ✅ Passed The changes directly address issue #201 by maintaining a persistent warmed AVAudioEngine, adding lifecycle/interruption handling, warming on resume, and synchronizing tick/chime timing with the sample rate from the active engine.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: AudioEngine refactoring, lifecycle/interruption handling, warm-up on resume, sample-rate synchronization, .gitignore update for build artifacts, and SessionViewModel integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #201

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-201-ios-chime-sync

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ios/StillPointApp/ViewModels/SessionViewModel.swift (1)

108-113: Skip warm-up when all session sounds are disabled.

Line 111 spins up the shared audio engine even when tick, chime, and completion are all off, which still pays the audio-session/lifecycle cost for a silent session. A small guard keeps the latency fix for audible sessions without doing unnecessary work here.

Suggested change
         isActive = true
         isPaused = false
         startDate = Date().addingTimeInterval(-pausedElapsed)
-        AudioEngine.shared.warmUp()
+        if soundPrefs.tick || soundPrefs.chime || soundPrefs.completion {
+            AudioEngine.shared.warmUp()
+        }
         startTimer()
         scheduleControlHide()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/ViewModels/SessionViewModel.swift` around lines 108 - 113,
Wrap the AudioEngine.shared.warmUp() call in a guard that only runs it when at
least one of the session sound flags (tick, chime, completion) is enabled; e.g.,
check the booleans named tick, chime, and completion and call
AudioEngine.shared.warmUp() only if tick || chime || completion is true, leaving
isActive, isPaused, startDate, startTimer(), and scheduleControlHide() as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointShared/Sources/StillPointShared/AudioEngine.swift`:
- Around line 247-259: The foreground restart must be gated on whether the
engine was running before backgrounding or an interruption indicated it should
resume: add a stored Bool (e.g., wasRunningBeforeBackground) and in
handleDidEnterBackground() set wasRunningBeforeBackground =
self.engine.isRunning before calling self.engine.pause(); then in
handleWillEnterForeground() only call self.ensureEngineRunning() if
wasRunningBeforeBackground || self.pendingResumeAfterConfigurationChange is true
(still call configureAudioSession() unconditionally), and clear/reset
wasRunningBeforeBackground appropriately after using it; update references to
self and weak captures as needed in handleDidEnterBackground and
handleWillEnterForeground to use this new flag.

---

Nitpick comments:
In `@ios/StillPointApp/ViewModels/SessionViewModel.swift`:
- Around line 108-113: Wrap the AudioEngine.shared.warmUp() call in a guard that
only runs it when at least one of the session sound flags (tick, chime,
completion) is enabled; e.g., check the booleans named tick, chime, and
completion and call AudioEngine.shared.warmUp() only if tick || chime ||
completion is true, leaving isActive, isPaused, startDate, startTimer(), and
scheduleControlHide() as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d1d0d14a-c021-42a8-a93d-74605dcc7d9a

📥 Commits

Reviewing files that changed from the base of the PR and between c5701c5 and 28168d5.

📒 Files selected for processing (3)
  • .gitignore
  • ios/StillPointApp/ViewModels/SessionViewModel.swift
  • ios/StillPointShared/Sources/StillPointShared/AudioEngine.swift

Comment thread ios/StillPointShared/Sources/StillPointShared/AudioEngine.swift
Comment thread ios/StillPointShared/Sources/StillPointShared/AudioEngine.swift
Only warm the audio engine when session sounds are enabled, and only restart playback on foreground if the engine was active before background or has a pending interruption resume.

Made-with: Cursor
Mirror generated samples across every channel buffer from AVAudioSourceNode so tick/chime playback remains audible and consistent on stereo output formats.

Made-with: Cursor
@auerbachb
Copy link
Copy Markdown
Owner Author

@cursor review

@auerbachb
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ecf2b17. Configure here.

}
self.wasRunningBeforeBackground = false
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale flag not cleared after foreground engine restart

Low Severity

handleWillEnterForeground uses pendingResumeAfterConfigurationChange to decide whether to restart the engine, but unlike resumeAfterInterruptionIfNeeded, it never clears the flag after a successful restart. This leaves the flag permanently stale, causing handleEngineConfigurationChange to unnecessarily call configureAudioSession() and ensureEngineRunning() on every future configuration-change notification.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ecf2b17. Configure here.

@auerbachb auerbachb merged commit 65819a3 into main Apr 22, 2026
6 checks passed
@auerbachb auerbachb deleted the issue-201-ios-chime-sync branch April 22, 2026 23:32
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.

iOS: Click/chime sound drifts out of sync with timer counter

1 participant