Skip to content

Add timed-set countdown cues and stronger rep sound#505

Merged
9thLevelSoftware merged 5 commits into
mainfrom
feat/490-sound-requests
Jun 7, 2026
Merged

Add timed-set countdown cues and stronger rep sound#505
9thLevelSoftware merged 5 commits into
mainfrom
feat/490-sound-requests

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

  • Emit countdown ticks for timed bodyweight sets during the last 10 seconds and vary playback speed near the end
  • Route rep-complete audio to a new stronger cue on Android and iOS
  • Update settings copy and add regression coverage for cue mapping and countdown suppression

Testing

  • Unit tests added for timed bodyweight countdown ticks and disabled countdown beeps
  • Existing audio routing guard coverage updated to verify the new rep-complete cue

Copilot AI review requested due to automatic review settings June 7, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements countdown beeps during the final 10 seconds of timed sets (for both bodyweight and cable exercises), with the playback rate increasing as the timer approaches zero. It introduces ExerciseCountdownCuePolicy to manage tick emission, updates user preferences and settings UI, and adds corresponding unit tests. The reviewer identified potential issues in ActiveSessionEngine.kt, including skipped initial countdown ticks for sets lasting 10 seconds or less, and potential race conditions from non-atomic timer decrements, recommending the use of MutableStateFlow.update for thread safety.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +2186 to +2187
var lastObservedRemaining = effectiveDuration
var lastTickedSecond = -1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For timed sets with a duration of 10 seconds or less (e.g., exactly 10 seconds or 5 seconds), the first countdown beep is skipped because the timer decrements before the first tick check is performed.

To fix this, initialize lastTickedSecond by checking and emitting a tick for the initial duration before entering the loop.

                        var lastObservedRemaining = effectiveDuration
                        var lastTickedSecond = emitExerciseCountdownTickIfNeeded(
                            remainingSeconds = effectiveDuration,
                            lastTickedSecond = -1,
                        )

Comment on lines +2196 to +2198
val current = coordinator._timedExerciseRemainingSeconds.value ?: 0
coordinator._timedExerciseRemainingSeconds.value = (current - 1).coerceAtLeast(0)
val remaining = (current - 1).coerceAtLeast(0)
coordinator._timedExerciseRemainingSeconds.value = remaining

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There is a potential race condition where a concurrent write to _timedExerciseRemainingSeconds (such as a timer reset or manual adjustment) can be overwritten by the non-atomic read-modify-write decrement operation.

Using MutableStateFlow.update ensures the decrement is atomic and prevents concurrent updates from being lost.

                                var remaining = 0
                                coordinator._timedExerciseRemainingSeconds.update { current ->
                                    remaining = ((current ?: 0) - 1).coerceAtLeast(0)
                                    remaining
                                }

Comment on lines +2486 to +2487
var lastObservedRemaining = timedExerciseDuration
var lastTickedSecond = -1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For timed sets with a duration of 10 seconds or less, the first countdown beep is skipped because the timer decrements before the first tick check is performed.

Initialize lastTickedSecond by checking and emitting a tick for the initial duration before entering the loop.

                        var lastObservedRemaining = timedExerciseDuration
                        var lastTickedSecond = emitExerciseCountdownTickIfNeeded(
                            remainingSeconds = timedExerciseDuration,
                            lastTickedSecond = -1,
                        )

Comment on lines +2495 to +2497
val current = coordinator._timedExerciseRemainingSeconds.value ?: 0
coordinator._timedExerciseRemainingSeconds.value = (current - 1).coerceAtLeast(0)
val remaining = (current - 1).coerceAtLeast(0)
coordinator._timedExerciseRemainingSeconds.value = remaining

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There is a potential race condition where a concurrent write to _timedExerciseRemainingSeconds (such as a timer reset or manual adjustment) can be overwritten by the non-atomic read-modify-write decrement operation.

Using MutableStateFlow.update ensures the decrement is atomic and prevents concurrent updates from being lost.

                                var remaining = 0
                                coordinator._timedExerciseRemainingSeconds.update { current ->
                                    remaining = ((current ?: 0) - 1).coerceAtLeast(0)
                                    remaining
                                }

@9thLevelSoftware 9thLevelSoftware linked an issue Jun 7, 2026 that may be closed by this pull request
2 tasks

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d4c33c16a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3860 to +3861
if (prefs.beepsEnabled) {
coordinator._hapticEvents.emit(HapticEvent.REST_ENDING)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect the countdown-beeps toggle at five seconds

When a user leaves general beeps on but disables countdown beeps, this new five-seconds-remaining branch still emits REST_ENDING, which maps to the rest-over audio/haptic cue. Since the settings copy now says countdown beeps cover the last 10 seconds of rest timers and timed sets, those users will still hear/get a cue during the disabled countdown window; gate this branch with countdownBeepsEnabled too, or keep REST_ENDING at actual rest completion.

Useful? React with 👍 / 👎.

}

try {
val playbackRate = if (event is HapticEvent.COUNTDOWN_TICK) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔥 The Roast: You added playback rate acceleration for countdown ticks in the SoundPool path, but completely forgot that playWithMediaPlayer exists as a fallback. On devices where SoundPool fails (looking at you, Fire OS), countdown ticks will play at normal speed like they're taking a leisurely stroll instead of racing to zero.

🩹 The Fix: Add playback rate support to playWithMediaPlayer when the event is a COUNTDOWN_TICK, matching what you did for SoundPool and iOS. Something like mediaPlayer.setPlaybackParams(PlaybackParams().setSpeed(ExerciseCountdownCuePolicy.playbackRate(event.secondsRemaining))) before mediaPlayer.start().

📏 Severity: warning

@kilo-code-bot

kilo-code-bot Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything.

The MediaPlayer fallback playback rate gap has been plugged. The peasants of Fire OS can now enjoy turbo-charged countdown beeps just like the SoundPool VIPs. It's like democracy for audio cues.

📊 Overall: Like finding a unicorn in production — the MediaPlayer fix actually landed. Someone pinch me.

Files Reviewed (8 files)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/presentation/components/HapticFeedbackEffect.android.kt - MediaPlayer fallback now has playback rate acceleration
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/components/HapticFeedbackAudioRoutingGuardTest.kt - Added test for MediaPlayer playback rate
  • shared/src/iosMain/kotlin/com/devil/phoenixproject/presentation/components/HapticFeedbackEffect.ios.kt - playback rate correctly applied
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ExerciseCountdownCuePolicy.kt - Clean policy object
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt - Timed set countdown logic implemented
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SettingsTab.kt - Label updated
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/ExerciseCountdownCuePolicyTest.kt - Comprehensive tests
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt - Integration tests for countdown ticks

Reviewed by laguna-m.1-20260312:free · 1,031,029 tokens

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73262bf80a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 3849 to 3851
if (!coordinator._isRestPaused.value &&
remainingSeconds in 1..10 &&
remainingSeconds in 6..10 &&
remainingSeconds != lastTickedSecond

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve existing rest countdown ticks

For routine rest timers with countdown beeps enabled, changing this range to 6..10 drops the existing ticks for 5, 4, 3, 2, and 1 seconds; before this change the rest loop emitted COUNTDOWN_TICK for the full last 10 seconds, and the Issue 490 plan explicitly says existing rest-timer countdown behavior should remain unchanged while adding timed-set cues. Users now only get 10–6 plus the separate rest-ending cue at 5 seconds, so the final five-second rest countdown no longer matches the setting copy or prior behavior.

Useful? React with 👍 / 👎.

9thLevelSoftware and others added 2 commits June 7, 2026 12:13
Resolved conflict in ActiveSessionEngine.kt where the PR branch reordered
kotlin.* imports while main added kotlinx.serialization imports — kept all four.

https://claude.ai/code/session_01F3GxXaEqPWv4ig8EmVCSMP
@9thLevelSoftware 9thLevelSoftware merged commit c53d1c2 into main Jun 7, 2026
9 checks passed
@9thLevelSoftware 9thLevelSoftware deleted the feat/490-sound-requests branch June 7, 2026 23:09
9thLevelSoftware pushed a commit that referenced this pull request Jun 7, 2026
Resolve merge conflicts with weight recommendation (PR #499),
timed-set countdown cues (PR #505), and Health Connect export (PR #516)
by keeping both deload modifiers and the new main features.

https://claude.ai/code/session_01MBWwVaUrnXjzZxYKKnn33i
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.

[Feature]: Couple of sound requests :-)

3 participants