Skip to content

Clock: use secs for better accuracy#32967

Open
RomanPudashkin wants to merge 6 commits intomusescore:masterfrom
RomanPudashkin:clock_use_secs
Open

Clock: use secs for better accuracy#32967
RomanPudashkin wants to merge 6 commits intomusescore:masterfrom
RomanPudashkin:clock_use_secs

Conversation

@RomanPudashkin
Copy link
Copy Markdown
Contributor

Based on: #32598

@RomanPudashkin RomanPudashkin added the playback General playback issue label Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

The PR converts the audio framework's timing API from msecs_t to secs_t across core interfaces (IClock, ISequencePlayer, IPlayer, IEnginePlayback, IGetPlaybackPosition) and implementations (Clock, Mixer, SequencePlayer, EnginePlayback, Player, SoundTrackWriter, etc.). IClock and Clock drop async notification endpoints (seekOccurred, statusChanged, countDownEnded) in favor of an ActionType enum and a single setOnAction(OnActionFunc) callback. Several headers switch to #pragma once. TrackSequence no longer owns a Clock; SequencePlayer now creates and manages one.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal, providing only a reference link to a related PR (32598) without addressing the template requirements such as issue resolution, motivation, CLA signature, or testing information. Complete the PR description by filling out the template sections: specify the resolved issue, explain the motivation and changes, check relevant boxes, and describe testing performed.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: converting the Clock API from millisecond-based types (msecs_t) to second-based types (secs_t) for better accuracy, which is the primary refactor across all modified files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framework/audio/engine/internal/clock.cpp`:
- Around line 48-56: When m_countDown <= secs, compute the remaining advance as
remainder = secs - m_countDown (capture m_countDown before zeroing it), set
m_countDown = 0, call onAction(ActionType::CountDownEnded, m_currentTime), and
then set newTime = m_currentTime + remainder instead of advancing by the full
secs; this ensures the tick only advances by the post-countdown remainder and
fixes the off-by-one-tick behavior.
- Around line 82-123: The callbacks invoked via onAction() from Clock methods
(start, stop, pause, resume, seek) capture this (SequencePlayer) and can race
with destruction because setOnAction(nullptr) is called in the destructor
without waiting; fix by introducing synchronization around the callback storage
and invocation: protect the internal callback with a mutex (or make it an
atomic/shared_ptr) updated in setOnAction and read under lock in onAction(), or
swap to a refcounted/shared_ptr inside setOnAction so onAction can copy the
shared_ptr and invoke it outside the lock; also ensure the destructor either
clears the callback under the same lock or waits for in-flight invocations to
finish (e.g., via a guard/refcount or condition variable) before returning.
Ensure references to methods: onAction, setOnAction, start, stop, pause, resume,
seek, and the SequencePlayer destructor are updated accordingly.

In `@src/framework/audio/engine/internal/sequenceplayer.cpp`:
- Around line 155-163: In SequencePlayer::setLoop, pass the requested new loop
start (the parameter from) into m_clock->setTimeLoop instead of using the old
m_loopStart, and only update m_loopStart to from when setTimeLoop succeeds;
i.e., call m_clock->setTimeLoop(from, to) and on successful Ret set m_loopStart
= from so the validation uses the requested range and not the previous value.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 20910562-fd30-476d-a59e-a3e5c17a35ec

📥 Commits

Reviewing files that changed from the base of the PR and between 7c68382 and 4354514.

📒 Files selected for processing (21)
  • src/framework/audio/engine/iclock.h
  • src/framework/audio/engine/iengineplayback.h
  • src/framework/audio/engine/internal/clock.cpp
  • src/framework/audio/engine/internal/clock.h
  • src/framework/audio/engine/internal/engineplayback.cpp
  • src/framework/audio/engine/internal/engineplayback.h
  • src/framework/audio/engine/internal/enginerpccontroller.cpp
  • src/framework/audio/engine/internal/export/soundtrackwriter.cpp
  • src/framework/audio/engine/internal/export/soundtrackwriter.h
  • src/framework/audio/engine/internal/igetplaybackposition.h
  • src/framework/audio/engine/internal/mixer.cpp
  • src/framework/audio/engine/internal/mixer.h
  • src/framework/audio/engine/internal/sequenceplayer.cpp
  • src/framework/audio/engine/internal/sequenceplayer.h
  • src/framework/audio/engine/internal/tracksequence.cpp
  • src/framework/audio/engine/internal/tracksequence.h
  • src/framework/audio/engine/isequenceplayer.h
  • src/framework/audio/main/internal/player.cpp
  • src/framework/audio/main/internal/player.h
  • src/framework/audio/main/iplayer.h
  • src/playback/internal/playbackcontroller.cpp
💤 Files with no reviewable changes (1)
  • src/framework/audio/engine/internal/tracksequence.h

Comment thread src/framework/audio/engine/internal/clock.cpp Outdated
Comment thread src/framework/audio/engine/internal/clock.cpp
Comment thread src/framework/audio/engine/internal/sequenceplayer.cpp
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: 2

♻️ Duplicate comments (2)
src/framework/audio/engine/internal/clock.cpp (2)

177-186: ⚠️ Potential issue | 🔴 Critical

Make callback teardown safe under concurrent dispatch.

setOnAction() mutates m_onActionFunc while onAction() reads and invokes it without synchronization. SequencePlayer::~SequencePlayer() clears the callback, but a proc-thread dispatch can still race and execute the stale lambda after SequencePlayer is destroyed.

#!/bin/bash
set -eu

echo "=== Clock callback storage/invocation ==="
sed -n '172,190p' src/framework/audio/engine/internal/clock.cpp

echo
echo "=== SequencePlayer callback registration and teardown ==="
sed -n '36,78p' src/framework/audio/engine/internal/sequenceplayer.cpp

echo
echo "=== Synchronization primitives around m_onActionFunc ==="
rg -n 'm_onActionFunc|mutex|lock_guard|unique_lock|shared_ptr|condition_variable|atomic' src/framework/audio/engine/internal/clock.*

Use shared state that survives in-flight calls, or add a teardown barrier so setOnAction(nullptr) cannot race with onAction().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/audio/engine/internal/clock.cpp` around lines 177 - 186, The
callback race is caused by unsynchronized mutation of m_onActionFunc in
Clock::setOnAction while Clock::onAction reads and calls it; fix by making the
stored callback reference-counted and copied before invocation (e.g. change
storage to std::shared_ptr<OnActionFunc> m_onActionFuncPtr, have setOnAction
create/reset that shared_ptr, and have onAction grab a local copy auto cb =
std::atomic_load(&m_onActionFuncPtr) or std::shared_ptr copy under a mutex and
then call cb if non-null), or alternatively add a teardown barrier so
setOnAction(nullptr) waits for in-flight onAction invocations to finish (use a
mutex + counter or condition_variable) — update Clock::setOnAction and
Clock::onAction accordingly and ensure SequencePlayer::~SequencePlayer() clears
the callback via the new safe API.

48-59: ⚠️ Potential issue | 🟠 Major

Advance only by the post-countdown remainder.

When m_countDown <= secs, Line 59 still advances by the full secs. That skips ahead by the portion of the block that should still have been spent waiting; the m_countDown == secs case is off by a full tick.

Proposed fix
-    if (!m_countDown.is_zero()) {
-        m_countDown -= secs;
-
-        if (m_countDown > 0.) {
-            return;
-        }
-
-        m_countDown = 0.;
-        onAction(ActionType::CountDownEnded, m_currentTime);
-    }
-
-    secs_t newTime = m_currentTime + secs;
+    secs_t elapsed = secs;
+
+    if (!m_countDown.is_zero()) {
+        if (m_countDown >= secs) {
+            m_countDown -= secs;
+            if (m_countDown.is_zero()) {
+                onAction(ActionType::CountDownEnded, m_currentTime);
+            }
+            return;
+        }
+
+        elapsed -= m_countDown;
+        m_countDown = 0.;
+        onAction(ActionType::CountDownEnded, m_currentTime);
+    }
+
+    secs_t newTime = m_currentTime + elapsed;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/audio/engine/internal/clock.cpp` around lines 48 - 59, The
countdown handling consumes part of the incoming secs but the code still
advances m_currentTime by the full secs; capture the original m_countDown at the
start of the block and when m_countDown <= secs compute the leftover time = secs
- original_countDown, set m_countDown = 0 and call
onAction(ActionType::CountDownEnded, m_currentTime), then advance time using
only the leftover instead of the full secs (use leftover when computing newTime
= m_currentTime + ...); if m_countDown > secs keep the current
subtract-and-return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framework/audio/engine/internal/clock.cpp`:
- Around line 119-126: Clock::seek currently returns early when newPosition ==
m_currentTime which suppresses same-position seek notifications; remove that
early return so Clock::seek always calls setCurrentTime(newPosition) and then
onAction(ActionType::Seek, m_currentTime) even when position is unchanged.
Ensure the function continues to use setCurrentTime and onAction as-is so
resume() and loop-rewind paths still emit ActionType::Seek to SequencePlayer for
resyncing.

In `@src/framework/audio/engine/internal/sequenceplayer.cpp`:
- Around line 61-63: The current LoopEndReached handler always seeks to
m_loopStart, dropping any overshoot; change it to preserve the remainder by
seeking to m_loopStart + (time - m_loopEnd). To implement this, ensure
SequencePlayer has access to the loop end/time (add m_loopEnd if not present) or
have Clock include the wrapped target time in the LoopEndReached action; then in
the case for IClock::ActionType::LoopEndReached compute wrapped = m_loopStart +
(action.time - m_loopEnd) and call m_clock->seek(wrapped) instead of
m_clock->seek(m_loopStart), keeping all other behavior the same.

---

Duplicate comments:
In `@src/framework/audio/engine/internal/clock.cpp`:
- Around line 177-186: The callback race is caused by unsynchronized mutation of
m_onActionFunc in Clock::setOnAction while Clock::onAction reads and calls it;
fix by making the stored callback reference-counted and copied before invocation
(e.g. change storage to std::shared_ptr<OnActionFunc> m_onActionFuncPtr, have
setOnAction create/reset that shared_ptr, and have onAction grab a local copy
auto cb = std::atomic_load(&m_onActionFuncPtr) or std::shared_ptr copy under a
mutex and then call cb if non-null), or alternatively add a teardown barrier so
setOnAction(nullptr) waits for in-flight onAction invocations to finish (use a
mutex + counter or condition_variable) — update Clock::setOnAction and
Clock::onAction accordingly and ensure SequencePlayer::~SequencePlayer() clears
the callback via the new safe API.
- Around line 48-59: The countdown handling consumes part of the incoming secs
but the code still advances m_currentTime by the full secs; capture the original
m_countDown at the start of the block and when m_countDown <= secs compute the
leftover time = secs - original_countDown, set m_countDown = 0 and call
onAction(ActionType::CountDownEnded, m_currentTime), then advance time using
only the leftover instead of the full secs (use leftover when computing newTime
= m_currentTime + ...); if m_countDown > secs keep the current
subtract-and-return behavior.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 5f1b615a-c161-4ab9-bb36-9f48ef3fa09d

📥 Commits

Reviewing files that changed from the base of the PR and between 4354514 and 072419e.

📒 Files selected for processing (19)
  • src/framework/audio/engine/iclock.h
  • src/framework/audio/engine/iengineplayback.h
  • src/framework/audio/engine/internal/clock.cpp
  • src/framework/audio/engine/internal/clock.h
  • src/framework/audio/engine/internal/engineplayback.cpp
  • src/framework/audio/engine/internal/engineplayback.h
  • src/framework/audio/engine/internal/enginerpccontroller.cpp
  • src/framework/audio/engine/internal/export/soundtrackwriter.cpp
  • src/framework/audio/engine/internal/export/soundtrackwriter.h
  • src/framework/audio/engine/internal/igetplaybackposition.h
  • src/framework/audio/engine/internal/mixer.cpp
  • src/framework/audio/engine/internal/mixer.h
  • src/framework/audio/engine/internal/sequenceplayer.cpp
  • src/framework/audio/engine/internal/sequenceplayer.h
  • src/framework/audio/engine/isequenceplayer.h
  • src/framework/audio/main/internal/player.cpp
  • src/framework/audio/main/internal/player.h
  • src/framework/audio/main/iplayer.h
  • src/playback/internal/playbackcontroller.cpp

Comment on lines +119 to +126
void Clock::seek(const secs_t newPosition)
{
if (m_currentTime == msecs) {
if (m_currentTime == newPosition) {
return;
}

setCurrentTime(msecs);
m_seekOccurred.notify();
setCurrentTime(newPosition);
onAction(ActionType::Seek, m_currentTime);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not suppress same-position seek notifications.

Line 121 turns seek(m_currentTime) into a no-op. That breaks the explicit resync in resume(), and loop rewinds can hit the same path when the loop starts at the current position, so SequencePlayer never receives ActionType::Seek to realign tracks.

Proposed fix
 void Clock::seek(const secs_t newPosition)
 {
-    if (m_currentTime == newPosition) {
-        return;
+    if (m_currentTime != newPosition) {
+        setCurrentTime(newPosition);
     }
-
-    setCurrentTime(newPosition);
-    onAction(ActionType::Seek, m_currentTime);
+    onAction(ActionType::Seek, newPosition);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void Clock::seek(const secs_t newPosition)
{
if (m_currentTime == msecs) {
if (m_currentTime == newPosition) {
return;
}
setCurrentTime(msecs);
m_seekOccurred.notify();
setCurrentTime(newPosition);
onAction(ActionType::Seek, m_currentTime);
void Clock::seek(const secs_t newPosition)
{
if (m_currentTime != newPosition) {
setCurrentTime(newPosition);
}
onAction(ActionType::Seek, newPosition);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/audio/engine/internal/clock.cpp` around lines 119 - 126,
Clock::seek currently returns early when newPosition == m_currentTime which
suppresses same-position seek notifications; remove that early return so
Clock::seek always calls setCurrentTime(newPosition) and then
onAction(ActionType::Seek, m_currentTime) even when position is unchanged.
Ensure the function continues to use setCurrentTime and onAction as-is so
resume() and loop-rewind paths still emit ActionType::Seek to SequencePlayer for
resyncing.

Comment on lines +61 to +63
case IClock::ActionType::LoopEndReached:
m_clock->seek(m_loopStart);
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve the loop remainder on wraparound.

Line 62 ignores the time carried by LoopEndReached and always snaps back to m_loopStart. If a processing block crosses the loop end by a partial chunk, that overshoot is dropped instead of wrapped, so every loop becomes shorter by up to one block. Store the loop end in SequencePlayer as well, or have Clock dispatch the wrapped target directly, then seek to m_loopStart + (time - m_loopEnd).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/audio/engine/internal/sequenceplayer.cpp` around lines 61 - 63,
The current LoopEndReached handler always seeks to m_loopStart, dropping any
overshoot; change it to preserve the remainder by seeking to m_loopStart + (time
- m_loopEnd). To implement this, ensure SequencePlayer has access to the loop
end/time (add m_loopEnd if not present) or have Clock include the wrapped target
time in the LoopEndReached action; then in the case for
IClock::ActionType::LoopEndReached compute wrapped = m_loopStart + (action.time
- m_loopEnd) and call m_clock->seek(wrapped) instead of
m_clock->seek(m_loopStart), keeping all other behavior the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

playback General playback issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant