Skip to content

Clock: use secs for better accuracy#32967

Open
RomanPudashkin wants to merge 1 commit intomusescore:masterfrom
RomanPudashkin:clock_use_secs
Open

Clock: use secs for better accuracy#32967
RomanPudashkin wants to merge 1 commit 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

This pull request converts the audio framework's timing system from millisecond-based to second-based units. Type signatures across clock, player, engine, and mixer interfaces change from msecs_t to secs_t. Parameter names are updated accordingly (e.g., msecsnewPosition, fromMsec/toMsecfrom/to). Internal arithmetic uses floating-point seconds instead of millisecond integer calculations. Method implementations, RPC handlers, and callers are updated to pass and handle seconds-based values. Header guard syntax changes to #pragma once. The time-changed notification channel renames from m_timeChangedInSecs to m_timeChanged.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and only references a related PR without providing context, objectives, or detailed explanation of changes made in this PR. Add a detailed description explaining the motivation for the change, the scope of modifications across files, and how switching to seconds-based timing improves accuracy.
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 PR title clearly and concisely describes the main change: switching the Clock implementation from milliseconds to seconds for improved timing accuracy.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

🤖 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.h`:
- Line 54: The override declaration for setTimeLoop in the clock implementation
uses parameter names "from" and "to" which drift from the IClock::setTimeLoop
parameter names; update the override signature Ret setTimeLoop(const secs_t
from, const secs_t to) override to use the same parameter identifiers as the
interface (rename to fromSec and toSec) so the header aligns with
IClock::setTimeLoop, or alternatively change the interface to remove the "Sec"
suffix across both declarations—ensure the parameter names match exactly between
IClock::setTimeLoop and this override.

In `@src/framework/audio/engine/internal/engineplayer.cpp`:
- Around line 118-123: The duration() method inconsistently null-checks m_clock
while the EnginePlayer constructor and sibling methods (setDuration, setLoop,
resetLoop, seek, play, pause, stop, resume, playbackPosition,
playbackPositionChanged, playbackStatus, playbackStatusChanged) assume m_clock
is non-null; remove the redundant guard in EnginePlayer::duration() and directly
call m_clock->timeDuration() so behavior matches the constructor and other
methods (ensure ONLY_AUDIO_ENGINE_THREAD remains).
🪄 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: fe39d6e9-8f5a-4960-b238-ccfe5f3f3a2b

📥 Commits

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

📒 Files selected for processing (18)
  • src/framework/audio/engine/iclock.h
  • src/framework/audio/engine/iengineplayback.h
  • src/framework/audio/engine/iengineplayer.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/engineplayer.cpp
  • src/framework/audio/engine/internal/engineplayer.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/main/internal/player.cpp
  • src/framework/audio/main/internal/player.h
  • src/framework/audio/main/iplayer.h

Ret setTimeLoop(const msecs_t fromMsec, const msecs_t toMsec) override;
secs_t timeDuration() const override;
void setTimeDuration(const secs_t duration) override;
Ret setTimeLoop(const secs_t from, const secs_t to) override;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Nit: parameter-name drift from IClock.

IClock::setTimeLoop declares parameters as fromSec, toSec (see src/framework/audio/engine/iclock.h), while the override here uses from, to. Legal, but aligning parameter names across interface and implementation reduces confusion when reading headers/docs side-by-side. Either rename here or drop the Sec suffix in the interface (the latter is more consistent with the rest of this PR, which drops unit suffixes from identifiers).

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

In `@src/framework/audio/engine/internal/clock.h` at line 54, The override
declaration for setTimeLoop in the clock implementation uses parameter names
"from" and "to" which drift from the IClock::setTimeLoop parameter names; update
the override signature Ret setTimeLoop(const secs_t from, const secs_t to)
override to use the same parameter identifiers as the interface (rename to
fromSec and toSec) so the header aligns with IClock::setTimeLoop, or
alternatively change the interface to remove the "Sec" suffix across both
declarations—ensure the parameter names match exactly between
IClock::setTimeLoop and this override.

Comment on lines +118 to 123
secs_t EnginePlayer::duration() const
{
ONLY_AUDIO_ENGINE_THREAD;

if (!m_clock) {
return 0;
}

return m_clock->timeDuration();
return m_clock ? m_clock->timeDuration() : secs_t { 0. };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Null-check in duration() is inconsistent with sibling methods.

The constructor unconditionally dereferences m_clock (lines 37-54), and every other method on EnginePlayer (setDuration, setLoop, resetLoop, seek, play, pause, stop, resume, playbackPosition, playbackPositionChanged, playbackStatus, playbackStatusChanged) dereferences m_clock without a guard. The class contract therefore already requires a non-null clock. Either drop the guard here (preferred, for consistency) or extend the same guard to the other methods if a null m_clock is genuinely reachable.

♻️ Proposed fix (drop the redundant guard)
 secs_t EnginePlayer::duration() const
 {
     ONLY_AUDIO_ENGINE_THREAD;

-    return m_clock ? m_clock->timeDuration() : secs_t { 0. };
+    return m_clock->timeDuration();
 }
📝 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
secs_t EnginePlayer::duration() const
{
ONLY_AUDIO_ENGINE_THREAD;
if (!m_clock) {
return 0;
}
return m_clock->timeDuration();
return m_clock ? m_clock->timeDuration() : secs_t { 0. };
}
secs_t EnginePlayer::duration() const
{
ONLY_AUDIO_ENGINE_THREAD;
return m_clock->timeDuration();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/audio/engine/internal/engineplayer.cpp` around lines 118 - 123,
The duration() method inconsistently null-checks m_clock while the EnginePlayer
constructor and sibling methods (setDuration, setLoop, resetLoop, seek, play,
pause, stop, resume, playbackPosition, playbackPositionChanged, playbackStatus,
playbackStatusChanged) assume m_clock is non-null; remove the redundant guard in
EnginePlayer::duration() and directly call m_clock->timeDuration() so behavior
matches the constructor and other methods (ensure ONLY_AUDIO_ENGINE_THREAD
remains).

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