Playback from position

Sorry about bombarding you with questions…

I’m using the following code (in a button handler) to start playback from the beginning of the edit. However, sometimes (in a seemingly random fashion) playback doesn’t start from the beginning but continues from the position where playback was last stopped.

I’m using a modified version of the EditComponent and there I can see that the playhead very briefly jumps to position 0 and then back to where it was.

void MainComponent::playFromTop()
{
    auto& transport = edit->getTransport();
    
    if (transport.isPlaying())
        transport.stop (false, false);
    
    transport.position = 0.0;
    transport.play (false);
}

Am I using the transport in the wrong way somehow?

That looks ok to me. Can you step through TransportControl::performPlay and see what time gets passed to playHeadWrapper->play on line 1268?

There’s a fair bit of logic to deal with limits, looping etc. in there.

The other thing to check would be that there isn’t any other part of your code changing the position?

Can you step through TransportControl::performPlay and see what time gets passed to playHeadWrapper->play on line 1268 ?

Ok, I added some logging to performPlay.

transportState->cursorPosAtPlayStart = position.get();
// => 0
...
playHeadWrapper->play ({ transportState->startTime, transportState->endTime }, looping);
// => { 0, 172800 }

The logged values are identical for each playback start, both when it actually starts from the beginning and when it continues from the previous position.

The other thing to check would be that there isn’t any other part of your code changing the position?

No, this is the only place in my code that sets the transport position.

Additional observations:

  • Logging confirms that the position is really set to 0 and then reset to the previous position a short time after playback has started

  • This happens in approx. 10% of the clicks on the play button (calling playFromTop quoted above)

  • The issue can be reproduced with only empty tracks

  • It is not a race between transport stopping and starting (as I first suspected): it is possible to get the same effect even when waiting a second or two between clicks on the “toggle play” button…

  • … but there is probably some kind of race going on, because if I insert a sleep() call between transport.position = 0.0 and transport.play(false) the “jumping” occurs less and less often the longer the sleep

I have a feeling it might be something subtle with the posting of the position change in to the audio graph (this needs to happen on the audio thread for it to be correctly in sync).

I’ve tried this hundreds of times this morning but only saw it once or twice and now can’t get it to happen at all. What I’m guessing is happening is:

  • You set the position and a call to TransportControl::performPositionChange happens
  • This gets the newly set position (0.0) and playHeadWrapper->setPosition (0.0)
  • There is a short delay whilst the next audio callback happens
  • In the mean time, the TransportControl::timerCallback triggers and line 974 gets called
  • This gets the current time from the playback graph and sets the transport position to it

There’s two potential solutions to this.

  1. Wait until the new position has been processed by the audio callback before proceeding in performPositionChange
  2. Avoid updating from the play graph for a short time after changing the position

Number 2. can be done quite easily by adding the following line to 1503 of tracktion_TransportControl.cpp:
transportState->lastUserDragTime = Time::getMillisecondCounter();

Can you add that and see if it fixes your problem? At least that will tell me if my guess is correct.

I tested it now and it does not fix the problem unfortunately!

Hmm, ok. I’ll need to figure out a better test so I can replicate it and see where the shift is coming from.

Let me know if I can be of any help! In my current setup I get this effect approximately 1 start out of 5. (Of course the rest of my enviroment could probably be affecting it, especially CPU load I guess)

I guess the most helpful thing would be a stack trace of when the second position change happens in TransportControl::performPositionChange. I.e. the one that moves the transport back away from 0.

At least that would hopefully tell me if it’s being read back out from the play head or not.

It looks like the second position change never reaches performPositionChange – there is never more than one log message per playback start printed from that function and it always prints my requested start position.

I also placed the following code in TransportControl::updatePositionFromPlayhead:

        auto oldPosition = (double)state[IDs::position];
        if (newPosition > (oldPosition + 1.0)) {
            juce::Logger::writeToLog("!!! LONG FORWARD JUMP " + juce::String(oldPosition) + " => " + juce::String(newPosition));
        } else if (newPosition < (oldPosition - 1.0)) {
            juce::Logger::writeToLog("!!! LONG BACKWARD JUMP " + juce::String(oldPosition) + " => " + juce::String(newPosition));
        }

Interestingly enough this message always gets printed when the playhead jumps but also sometimes when no other indication is given that the playhead jumps. So can it be that the playhead jumps forward to the old position and then back again to the requested position?!

Here is a log and backtrace from that place:

changeListenerCallback (song transport)
changeListenerCallback (song transport)
performPositionChange 4 -> 4
Position after setting: 4
In performPlay (1): 4
changeListenerCallback (song transport)
!!! LONG FORWARD JUMP 4 => 6.29878
(lldb) bt
* thread #1, name = 'JUCE Message Thread', queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100f70143 Gaudiamus`tracktion_engine::TransportControl::TransportState::updatePositionFromPlayhead(this=0x000000010589b600, newPosition=6.2987755102040817) at tracktion_TransportControl.cpp:166
    frame #1: 0x0000000100f6f740 Gaudiamus`tracktion_engine::TransportControl::timerCallback(this=0x0000000103f213a0) at tracktion_TransportControl.cpp:983
    frame #2: 0x00000001006920e2 Gaudiamus`juce::Timer::TimerThread::callTimers(this=0x0000000103e06e30) at juce_Timer.cpp:114
    frame #3: 0x0000000100691fb6 Gaudiamus`juce::Timer::TimerThread::CallTimersMessage::messageCallback(this=0x0000600000002b60) at juce_Timer.cpp:180
    frame #4: 0x000000010069998e Gaudiamus`juce::MessageQueue::deliverNextMessage(this=0x00006040000c1650) at juce_osx_MessageQueue.h:81
    frame #5: 0x00000001006998d6 Gaudiamus`juce::MessageQueue::runLoopCallback(this=0x00006040000c1650) at juce_osx_MessageQueue.h:92
    frame #6: 0x00000001006997a5 Gaudiamus`juce::MessageQueue::runLoopSourceCallback(info=0x00006040000c1650) at juce_osx_MessageQueue.h:100
    frame #7: 0x00007fff2a3a6691 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #8: 0x00007fff2a460c8c CoreFoundation`__CFRunLoopDoSource0 + 108
    frame #9: 0x00007fff2a3890f0 CoreFoundation`__CFRunLoopDoSources0 + 208
    frame #10: 0x00007fff2a38856d CoreFoundation`__CFRunLoopRun + 1293
    frame #11: 0x00007fff2a387dd3 CoreFoundation`CFRunLoopRunSpecific + 483
    frame #12: 0x00007fff29672d96 HIToolbox`RunCurrentEventLoopInMode + 286
    frame #13: 0x00007fff29672b06 HIToolbox`ReceiveNextEventCommon + 613
    frame #14: 0x00007fff29672884 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 64
    frame #15: 0x00007fff2791fa3b AppKit`_DPSNextEvent + 2085
    frame #16: 0x00007fff280b5e34 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 3044
    frame #17: 0x00007fff2791484d AppKit`-[NSApplication run] + 764
    frame #18: 0x000000010067c8d2 Gaudiamus`juce::MessageManager::runDispatchLoop(this=0x0000604000249240) at juce_mac_MessageManager.mm:362
    frame #19: 0x000000010067c6c4 Gaudiamus`juce::JUCEApplicationBase::main() at juce_ApplicationBase.cpp:263
    frame #20: 0x000000010067c28c Gaudiamus`juce::JUCEApplicationBase::main(argc=3, argv=0x00007ffeefbff770) at juce_ApplicationBase.cpp:241
    frame #21: 0x0000000100161123 Gaudiamus`main(argc=3, argv=0x00007ffeefbff770) at Main.cpp:160
    frame #22: 0x00007fff5234f015 libdyld.dylib`start + 1

If you change postPosition to the following does that fix it?
I’m trying to see if getting the playhead fully in sync on the message thread fixes the problem.

     void postPosition (double newPosition)
     {
         pendingPosition.store (newPosition, std::memory_order_release);
         pendingRollInToLoop.store (false, std::memory_order_release);
         positionUpdatePending = true;
         
         while (positionUpdatePending.load())
             juce::Thread::sleep (1);
     }

The other thing to check is if you have any plugins in your graph that introduce latency?
I’m wondering if there’s a clash between the “live time” (i.e. audible after all the latency delays) and the actual time being played in the graph.

You can find this out by putting a breakpoint on line 163 of tracktion_EditPlaybackContext.cpp and finding out what value was assigned to latencySamples.


This is all fiddly internals but should “just work” to the visible surface of the API. The live position shouldn’t trigger any actual transport repositioning and should be cleared in the playback graph when an explicit position is set.

Thanks! I tried your new postPosition now and it seems like that may have solved the problem!

I do still get a few log messages like this (coming from the logging code I inserted earlier, see above):

performPositionChange 4 -> 4
Position after setting: 4
In performPlay (1): 4
changeListenerCallback (song transport)
!!! LONG FORWARD JUMP 4 => 5.14939
!!! LONG BACKWARD JUMP 5.14939 => 4.04644

So far these now always seem to come in “pairs” so that the playhead jumps forward (to the position where playback was last stopped) and then immediately back. This “double jump” is so brief that it doesn’t give any graphical artifacts. I have not been able to reproduce the original problem where the playhead jumps forward and then continues from there.

All plugins used report latencySamples = 0. (I also tried removing all plugins but the default volume and level meter but it does not make any difference apart from maybe making the jumps occur less frequently)

You don’t have SettingID::resetCursorOnStop on by any chance do you? I doubt it or you probably would have mentioned it.

I’d still like to be able to replicate this reliably as it feels like I don’t have a full idea of the root cause and hence the correct solution…

Maybe rather than the position change code waiting for the postPosition to become visible, the updatePositionFromPlayhead should wait until all pending changes have been made. That sounds a bit cleaner to me.

No

Maybe rather than the position change code waiting for the postPosition to become visible, the updatePositionFromPlayhead should wait until all pending changes have been made. That sounds a bit cleaner to me.

Ok! How would I test for that?

Does applying this patch work for you?
position_change.diff (3.9 KB)

Sorry, that loop in postPosition snuck in to that patch. You’ll have to remove or comment that out to test the new method.

Ok, I’ll try it out ASAP!

I’m sorry but no: it does not solve the problem. :cry: (However I was pretty tired now so I will double check tomorrow that I applied the patch correctly and all)

(Sidenote: I think the waiting in postPosition that I tried earlier caused the application to enter some kind of deadlock; I had to force quit it a couple of times. The debugger indicated that it was stuck in that loop, at least the one time I checked. But now since that code was removed anyway it is no longer a problem)

The only way I can think it will spin forever is if the audio callback for that Edit is never called… It was just a quick hack to see if that was indeed the problem though. It would need some kind of timeout or fallback for production.

There may well be a bug in the other approach. It’s a bit fiddly and I’m not keen on it.


Just to refresh me on the original problem, does the transport actually move back to the non-zero position and continue or is it just a flash for a frame or two? I.e. does it go back to the start soon after?

Just to refresh me on the original problem, does the transport actually move back to the non-zero position and continue or is it just a flash for a frame or two? I.e. does it go back to the start soon after?

Both. In the cases where it jumps forward and back again it isn’t always noticable apart from the log messages.