# Playhead sync

**URL:** <https://forum.juce.com/t/playhead-sync/67779>\
**Category:** Tracktion Engine\
**Created:** [December 11, 2025, 12:55pm UTC](https://forum.juce.com/t/playhead-sync/67779 "2025-12-11T12:55:45Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 11, 2025, 12:55pm UTC](https://forum.juce.com/t/playhead-sync/67779/1 "2025-12-11T12:55:45Z")

</div>

Good morning,

I have my tracktion engine application that has a timeline. I have a playhead that move smooth when running but when I do “PAUSE” (transport.stop(false, false)) she do a small jump. It seams that there is a problem with the sync.

I have a PositionManager that manage the position of the playhead in this way:

PositionManager.h

```auto
#pragma once

#include <JuceHeader.h>
#include "tracktion_engine/tracktion_engine.h"
#include <atomic>
#include <cmath>

class PositionManager : public juce::ChangeBroadcaster,
    private juce::AudioIODeviceCallback,
    private juce::AsyncUpdater,
    private juce::ChangeListener
{
public:
    PositionManager(tracktion::engine::Edit& editRef, tracktion::engine::TransportControl& tc, juce::AudioDeviceManager& dm);
    ~PositionManager() override;

    double getCurrentTime() const;

    void stopTransport()
    {
        transport.stop(false, false);
    }

private:
    void audioDeviceIOCallbackWithContext(const float* const* inputChannelData,
        int numInputChannels,
        float* const* outputChannelData,
        int numOutputChannels,
        int numSamples,
        const juce::AudioIODeviceCallbackContext& context) override;
    void audioDeviceAboutToStart(juce::AudioIODevice*) override;
    void audioDeviceStopped() override;

    void handleAsyncUpdate() override;
    void changeListenerCallback(juce::ChangeBroadcaster* source) override;
    double getCurrentAudibleTimeSeconds() const;

    tracktion::engine::Edit& edit;
    tracktion::engine::TransportControl& transport;
    juce::AudioDeviceManager& deviceManager;
    std::atomic<double> currentTime{ 0.0 };
    std::atomic<bool> forceBroadcastPending{ false };
    bool wasTransportPlaying = false;
    double lastBroadcastedTime = 0.0;
    double lastKnownSampleRate = 0.0;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PositionManager)
};

```

PositionManager.cpp

```auto
#include "PositionManager.h"

PositionManager::PositionManager(tracktion::engine::Edit& editRef,
    tracktion::engine::TransportControl& tc,
    juce::AudioDeviceManager& dm)
    : edit(editRef), transport(tc), deviceManager(dm)
{
    currentTime.store(getCurrentAudibleTimeSeconds(), std::memory_order_relaxed);
    wasTransportPlaying = transport.isPlaying();
    transport.addChangeListener(this);
    deviceManager.addAudioCallback(this);
}

PositionManager::~PositionManager()
{
    cancelPendingUpdate();
    transport.removeChangeListener(this);
    deviceManager.removeAudioCallback(this);
}

double PositionManager::getCurrentTime() const
{
    return currentTime.load(std::memory_order_relaxed);
}

void PositionManager::audioDeviceIOCallbackWithContext(const float* const* inputChannelData,
    int numInputChannels,
    float* const* outputChannelData,
    int numOutputChannels,
    int numSamples,
    const juce::AudioIODeviceCallbackContext&)
{
    juce::ignoreUnused(inputChannelData, numInputChannels, outputChannelData, numOutputChannels, numSamples);
    const bool isPlaying = transport.isPlaying();
    const bool transportStateChanged = isPlaying != wasTransportPlaying;
    wasTransportPlaying = isPlaying;
    const double newTime = getCurrentAudibleTimeSeconds();
    currentTime.store(newTime, std::memory_order_relaxed);
    const double minDelta = (numSamples > 0 && lastKnownSampleRate > 0.0)
        ? static_cast<double>(numSamples) / lastKnownSampleRate
        : 0.0;
    const double timeDelta = std::abs(newTime - lastBroadcastedTime);

    if (transportStateChanged && !isPlaying)
    {
        forceBroadcastPending.store(true, std::memory_order_release);
        triggerAsyncUpdate();
        return;
    }

    if (isPlaying && timeDelta < minDelta)
        return;

    if (!isPlaying && timeDelta < 1e-6)
        return;

    triggerAsyncUpdate();
}

void PositionManager::audioDeviceAboutToStart(juce::AudioIODevice*)
{
    currentTime.store(getCurrentAudibleTimeSeconds(), std::memory_order_relaxed);
    wasTransportPlaying = transport.isPlaying();
    lastKnownSampleRate = deviceManager.getCurrentAudioDevice() != nullptr
        ? deviceManager.getCurrentAudioDevice()->getCurrentSampleRate()
        : 0.0;
}

void PositionManager::audioDeviceStopped()
{
    lastKnownSampleRate = 0.0;
}

void PositionManager::handleAsyncUpdate()
{
    const bool forceBroadcast = forceBroadcastPending.exchange(false, std::memory_order_acq_rel);
    const double newTime = currentTime.load(std::memory_order_relaxed);

    if (forceBroadcast || std::abs(newTime - lastBroadcastedTime) > 1e-6)
    {
        lastBroadcastedTime = newTime;
        sendChangeMessage();
    }
}

void PositionManager::changeListenerCallback(juce::ChangeBroadcaster* source)
{
    if (source != &transport)
        return;

    currentTime.store(transport.getPosition().inSeconds(), std::memory_order_relaxed);
    forceBroadcastPending.store(true, std::memory_order_release);
    triggerAsyncUpdate();
}

double PositionManager::getCurrentAudibleTimeSeconds() const
{
    return transport.getPosition().inSeconds();
}

```

then in my SequencerComponent I have this in a

void SequencerComponent::changeListenerCallback(juce::ChangeBroadcaster\* source):

```auto
 const double timeSec = positionManager.getCurrentTime();
 updatePlayheadFromTime(timeSec, !isFollowingPlayhead);

```

Maybe I’m doing all wrong but someone can help to achieve a real time playhead?

Thanks  
Luca

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 11, 2025, 1:08pm UTC](https://forum.juce.com/t/playhead-sync/67779/2 "2025-12-11T13:08:47Z")

</div>

In the meanwhile I found using:

```auto
double PositionManager::getCurrentAudibleTimeSeconds() const
{
    if (auto epc = transport.getCurrentPlaybackContext())
    {
        auto position = epc->getPosition();
        return position.inSeconds();
    }
}

```

Instead of:

```auto
double PositionManager::getCurrentAudibleTimeSeconds() const
{
    return transport.getPosition().inSeconds();
}

```

Seams to fix the problem 😮

---

<div class="post-metadata">

**Author:** ![daniel](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/daniel/32/790_2.png) [@daniel](https://forum.juce.com/u/daniel)\
**Post date:** [December 11, 2025, 1:52pm UTC](https://forum.juce.com/t/playhead-sync/67779/3 "2025-12-11T13:52:46Z")

</div>

> [@THM](#):
>
> `auto position = epc->getPosition();`

Most likely because this line creates an unwanted copy.

You could try out of curiosity to change to:

```auto
auto& position = epc->getPosition();

```

I don’t have the tracktion sources at hand…

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 11, 2025, 2:14pm UTC](https://forum.juce.com/t/playhead-sync/67779/4 "2025-12-11T14:14:23Z")

</div>

I ended up with this simpler code and it works:

PositionManager.h

```auto
#pragma once

#include <JuceHeader.h>
#include "tracktion_engine/tracktion_engine.h"
#include <atomic>
#include <cmath>

class PositionManager : public juce::ChangeBroadcaster,
    private juce::HighResolutionTimer,
    private juce::ChangeListener,
    private juce::AsyncUpdater
{
public:
    PositionManager(tracktion::engine::Edit& editRef, tracktion::engine::TransportControl& tc);
    ~PositionManager() override;

    double getCurrentTime() const;

    void stopTransport()
    {
        transport.stop(false, false);
    }

private:
    void handleAsyncUpdate() override;
    void changeListenerCallback(juce::ChangeBroadcaster* source) override;
    void hiResTimerCallback() override;

    tracktion::engine::Edit& edit;
    tracktion::engine::TransportControl& transport;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PositionManager)
};

```

PositionManger.cpp

```auto
#include "PositionManager.h"

PositionManager::PositionManager(tracktion::engine::Edit& editRef,
    tracktion::engine::TransportControl& tc)
    : edit(editRef), transport(tc)
{
    transport.addChangeListener(this);
    startTimer(10);
}

PositionManager::~PositionManager()
{
    cancelPendingUpdate();
    transport.removeChangeListener(this);
    stopTimer();
}

double PositionManager::getCurrentTime() const
{
    if (auto epc = transport.getCurrentPlaybackContext())
    {
        return epc->getPosition().inSeconds();
    }

    return transport.getPosition().inSeconds();
}

void PositionManager::handleAsyncUpdate()
{
    sendChangeMessage();
}

void PositionManager::changeListenerCallback(juce::ChangeBroadcaster* source)
{
    if (source != &transport)
        return;

    triggerAsyncUpdate();
}

void PositionManager::hiResTimerCallback()
{
    triggerAsyncUpdate();
}

```

**Not sure if is better or not to use handleAsyncUpdate.**

Thanks!

---

<div class="post-metadata">

**Author:** ![dave96](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/dave96/32/8487_2.png) [@dave96](https://forum.juce.com/u/dave96)\
**Post date:** [December 11, 2025, 8:17pm UTC](https://forum.juce.com/t/playhead-sync/67779/5 "2025-12-11T20:17:52Z")

</div>

> [@daniel](#):
>
> ```auto
> auto& position = epc->getPosition();
> 
> ```

Don’t do that, `getPosition()` returns a `TimePosition` which is just a wrapper around a `double`, it’s just a value type.

The current time is flushed to the `TransportControl` on a timer so may be slightly out of sync. The `EditPlaybackContext` is updated in the audio callback so will be most up to date. But you might want to look at `EditPlaybackContext::getAudibleTimelineTime()` which also adjusts for latency introduced by PDC.

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 11, 2025, 8:32pm UTC](https://forum.juce.com/t/playhead-sync/67779/6 "2025-12-11T20:32:38Z")

</div>

Hi dave,

I’m confused. Why my last post working correct and how should modify it in order to do stuff in the right way?

Thanks!

---

<div class="post-metadata">

**Author:** ![dave96](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/dave96/32/8487_2.png) [@dave96](https://forum.juce.com/u/dave96)\
**Post date:** [December 11, 2025, 8:45pm UTC](https://forum.juce.com/t/playhead-sync/67779/7 "2025-12-11T20:45:05Z")

</div>

What you’ve done will work until you have plugins that introduce latency. Then your playhead will be in front of the audible content. Using `getAudibleTimelineTime` effectively shifts this back in time by the total latency amount. (Except it’s a bit more complicated than that).

---

<div class="post-metadata">

**Author:** ![daniel](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/daniel/32/790_2.png) [@daniel](https://forum.juce.com/u/daniel)\
**Post date:** [December 11, 2025, 8:47pm UTC](https://forum.juce.com/t/playhead-sync/67779/8 "2025-12-11T20:47:58Z")

</div>

> [@dave96](#):
>
> Don’t do that, `getPosition()` returns a `TimePosition` which is just a wrapper around a `double`, it’s just a value type.

Apologies, I should have checked the sources 😊

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 11, 2025, 9:15pm UTC](https://forum.juce.com/t/playhead-sync/67779/9 "2025-12-11T21:15:40Z")

</div>

> [@THM](#):
>
> ```auto
> double PositionManager::getCurrentTime() const
> {
> if (auto epc = transport.getCurrentPlaybackContext())
> {
> return epc->getPosition().inSeconds();
> }
> 
> return transport.getPosition().inSeconds();
> }
> 
> ```

So here I should use EditPlaybackContext::getAudibleTimelineTime()?

---

<div class="post-metadata">

**Author:** ![dave96](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/dave96/32/8487_2.png) [@dave96](https://forum.juce.com/u/dave96)\
**Post date:** [December 11, 2025, 9:35pm UTC](https://forum.juce.com/t/playhead-sync/67779/10 "2025-12-11T21:35:59Z")

</div>

Yes

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 12, 2025, 4:14pm UTC](https://forum.juce.com/t/playhead-sync/67779/11 "2025-12-12T16:14:09Z")

</div>

Something like that:

```auto
double PositionManager::getCurrentTime() const
{
    if (auto epc = transport.getCurrentPlaybackContext())
    {
        return epc->getAudibleTimelineTime().inSeconds();
    }
    return transport.getPosition().inSeconds();
}

```

In this way when I click pause I have a small jump of the timeline ☹

---

<div class="post-metadata">

**Author:** ![dave96](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/dave96/32/8487_2.png) [@dave96](https://forum.juce.com/u/dave96)\
**Post date:** [December 12, 2025, 4:44pm UTC](https://forum.juce.com/t/playhead-sync/67779/12 "2025-12-12T16:44:45Z")

</div>

Looking at Waveform, we just use `TransportControl::getPosition()` which handles all that latency internally.

Is there a reason you’re not just reading that directly on the message thread?

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 15, 2025, 9:47am UTC](https://forum.juce.com/t/playhead-sync/67779/13 "2025-12-15T09:47:37Z")

</div>

When I use return transport.getPosition().inSeconds(); and I press pause/play it jump a little bit like is not really synced.

---

<div class="post-metadata">

**Author:** ![dave96](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/dave96/32/8487_2.png) [@dave96](https://forum.juce.com/u/dave96)\
**Post date:** [December 15, 2025, 9:57am UTC](https://forum.juce.com/t/playhead-sync/67779/14 "2025-12-15T09:57:16Z")

</div>

Are you continuously drawing it or only when it is playing?

Have you looked at the demo apps? I don’t think they have this problem do they?

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 15, 2025, 2:10pm UTC](https://forum.juce.com/t/playhead-sync/67779/15 "2025-12-15T14:10:01Z")

</div>

you mean this one [tracktion\_engine/examples/DemoRunner/demos/ClipLauncherDemo.h at 59a3b72e50390ec04c18bbdf4440e66574d00650 · Tracktion/tracktion\_engine · GitHub](https://github.com/Tracktion/tracktion_engine/blob/59a3b72e50390ec04c18bbdf4440e66574d00650/examples/DemoRunner/demos/ClipLauncherDemo.h) ?

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 15, 2025, 2:46pm UTC](https://forum.juce.com/t/playhead-sync/67779/16 "2025-12-15T14:46:46Z")

</div>

I tried this, a super simple window with:

```auto
#pragma once

#include <juce_gui_basics/juce_gui_basics.h>
#include <tracktion_engine/tracktion_engine.h>

class Sequencer2Component : public juce::Component,
    private juce::Timer
{
public:
    explicit Sequencer2Component(tracktion::engine::TransportControl& transportToUse);

    void paint(juce::Graphics& g) override;
    void resized() override;

private:
    void timerCallback() override;

    tracktion::engine::TransportControl& transport;

    double pixelsPerSecond = 200.0;
    int playheadX = 0;
};

```

and:

```auto
#include "Sequencer2Component.h"
#include <cmath>

Sequencer2Component::Sequencer2Component(tracktion::engine::TransportControl& transportToUse)
    : transport(transportToUse)
{
    setOpaque(true);
    startTimerHz(60);
}

void Sequencer2Component::resized()
{
    playheadX = (int)std::lround(getWidth() * 0.4);
}

void Sequencer2Component::paint(juce::Graphics& g)
{
    g.fillAll(juce::Colours::black);

    const double seconds = transport.getPosition().inSeconds();
    const double scrollOffsetX = seconds * pixelsPerSecond;

    {
        juce::Graphics::ScopedSaveState state(g);
        g.addTransform(juce::AffineTransform::translation((float)(-scrollOffsetX + playheadX), 0.0f));

        g.setColour(juce::Colours::darkgrey);

        for (int i = 0; i < 2000; ++i)
        {
            const float x = (float)(i * 100);
            g.drawLine(x, 0.0f, x, (float)getHeight());
        }
    }

    g.setColour(juce::Colours::white);
    g.fillRect(playheadX, 0, 2, getHeight());

    g.setFont(14.0f);
    g.drawText("t(s): " + juce::String(seconds, 6) +
        " | playing: " + juce::String(transport.isPlaying() ? "YES" : "NO"),
        10, 10, getWidth() - 20, 20, juce::Justification::left);
}

void Sequencer2Component::timerCallback()
{
    repaint();
}

```

and also here I have a jump when I click pause:

```auto
transport.stop(false, false);

```

But I still have this jump

---

<div class="post-metadata">

**Author:** ![dave96](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/dave96/32/8487_2.png) [@dave96](https://forum.juce.com/u/dave96)\
**Post date:** [December 17, 2025, 11:42am UTC](https://forum.juce.com/t/playhead-sync/67779/17 "2025-12-17T11:42:40Z")

</div>

Did you try the demo examples in TE itself?

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 17, 2025, 3:05pm UTC](https://forum.juce.com/t/playhead-sync/67779/18 "2025-12-17T15:05:00Z")

</div>

The problem is that I’m running it inside a raspberry with 4 inch monitor and I see only “Select a demo above to begin” others button are hided 😃 I need to check the code and try to modify or find a way to start directly the correct demo!

---

<div class="post-metadata">

**Author:** ![THM](https://avatars.discourse-cdn.com/v4/letter/t/6a8cbe/32.png) [@THM](https://forum.juce.com/u/THM)\
**Post date:** [December 17, 2025, 4:04pm UTC](https://forum.juce.com/t/playhead-sync/67779/19 "2025-12-17T16:04:13Z")

</div>

I achieved to run MidiRecording and the playhead run correctly so I tried a really simple component like this:

```auto
#pragma once

#include <juce_gui_basics/juce_gui_basics.h>
#include <tracktion_engine/tracktion_engine.h>

class Sequencer2Component : public juce::Component,
    private juce::Timer
{
public:
    explicit Sequencer2Component(tracktion::engine::TransportControl& transportToUse);

    void paint(juce::Graphics& g) override;
    void resized() override;

private:
    void timerCallback() override;

    tracktion::engine::TransportControl& transport;

    float pixelsPerSecond = 100.0f;
};

```

```auto
#include "Sequencer2Component.h"
#include <cmath>

Sequencer2Component::Sequencer2Component(tracktion::engine::TransportControl& transportToUse)
    : transport(transportToUse)
{
    setOpaque(true);
    startTimerHz(60);
}

void Sequencer2Component::resized()
{

}

void Sequencer2Component::paint(juce::Graphics& g)
{
    g.fillAll(juce::Colours::black);

    const double timeSec = transport.getPosition().inSeconds();

    const int playheadX = (int)std::lround(timeSec * pixelsPerSecond);

    g.setColour(juce::Colours::white);
    g.fillRect(playheadX, 0, 2, getHeight());

    g.setColour(juce::Colours::white);
    g.setFont(14.0f);
    g.drawText("t(s): " + juce::String(transport.getPosition().inSeconds(), 6)
        + " | playing: " + juce::String(transport.isPlaying() ? "YES" : "NO"),
        10, 10, getWidth() - 20, 20, juce::Justification::left);
}

void Sequencer2Component::timerCallback()
{
    repaint();
}

```

and I have the same problem of start/pause with the jump.  
What I’m doing wrong? 😮

---

<div class="post-metadata">

**Author:** ![baramgb](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.juce.com/baramgb/32/12822_2.png) [@baramgb](https://forum.juce.com/u/baramgb)\
**Post date:** [December 17, 2025, 4:50pm UTC](https://forum.juce.com/t/playhead-sync/67779/20 "2025-12-17T16:50:30Z")

</div>

Have you tried to use float for the playhead pixel Position? Juce can draw float rectangle, too. But i’m not sure if this fix your issue. Also your rounding looks a bit tricky. Perhaps it jumps this one pixel that not fit?

[Next page](https://forum.juce.com/t/playhead-sync/67779.md?page=2)
