See repro steps. It helps to be running a debug build or other constraint that will make it more likely for the frame rate to be bad enough to fail to submit.
Alternative solution: Quartz could allow me to specify a specific beat to play on, and if I “miss” it it could fail. That’s not great… I’d need to continually retry until success with slightly different start times, but it would help.
Steps to Reproduce
Start with some tracks for a single piece of music.
Start a quartz clock, and then start one of them synchronized to the bar.
Say you want to start a second one halfway through the bar. You could figure out what the next beat is, do the math to adjust “StartTime” to start at that beat, and call PlayQuantized with TransportRelative, Beat boundary, and multiplier 1.
But if you’re too close to the beat, then your command won’t clear the queue to the other thread before the beat occurs, in which case it will queue for the next beat, and StartTime will be a beat off.
Desired solution: callback from the audio thread that allows me to adjust StartTime to the actual clock at buffer submission.
Quartz lets you schedule ahead, so you do need enough lead time for the command to execute. That being said, people get a way with fairly minimal lead time (down to a 1/16th note).
If you want to make sure you have enough time you can use Quartz to query how much time there is until the next boundary and make a decision in the BP.
>“I’d need to continually retry until success with slightly different start times, but it would help.”
What is the desired fallback behavior if your deadline is going to be missed? Would using Current Time Relative or Bar Relative work? (i.e. you want to make sure the stem starts on beat 2 of a bar, either this bar or the next).
>" callback from the audio thread that allows me to adjust StartTime to the actual clock at buffer submission."
Out of curiosity, what info would that callback provide? Would this be calling back to your BP or do you have a C++ implementation?
What is the desired fallback behavior if your deadline is going to be missed? Call the delegate callback with a “deadline missed” error condition (aka the submitted request wasn’t processed in time to make the hard deadline.) This prevents music from playing under the wrong conditions. I suspect I can do this right now with transport relative mode, but I haven’t tested it. The point is in current position mode if I say “play next beat” expecting next beat to be beat 2 but I have a lag spike and the audio thread doesn’t process until beat 3 I’m a beat off.
Out of curiosity, what info would that callback provide? It gives me the opportunity to do math and give you the correct StartTime offset, instead of assuming the one handed in from the game thread is correct. What I’m seeing right now is that if I want to sync up a piece of music that’s 8 bars long with one that’s already playing that’s 8 bars long it’s very difficult to align without waiting until the next full loop boundary. My current approach is to queue the new track for “next beat” and do math to calculate how far to skip into the sound file in order to start playback on the new file from the position that aligns with what “next beat” ought to be. The problem is that when I miss that deadline with the background audio thread, it’ll correctly align to the “next next beat”, but then play a beat offset because the math I did is out of date. If I got a callback from the audio thread just before it wrote to the outgoing buffer, I could update StartTime to be correct.
Would this be calling back to your BP or do you have a C++ implementation? Given the likelihood of badness having multiple threads enter BP, I would assume this would only be available to C++ implementations. You could solve this with additional information in PlayQuantized that would let the audio thread do the StartTime calculation instead.
The core issue is all the quantization boundaries are on multiplies of the boundary they represent. So there is no way for Quartz to know if you’re late to your desired deadline or early. It is essentially a quantizer.
The best you can do is use the existing helpers to determine yourself the time until the next musical duration and do that logic from the game before calling PlayQuantized
(i.e. GetBeatProgressPercent,
GetDurationOfQuantizationTypeInSeconds,
GetCurrentClockTimestamp, etc.)
You can also query the latency heuristic to see how long commands are taking on avg to get to the Audio Render Thread (GetGameThreadToAudioRenderThread[Average/Min/Max]Latency,
If you wanted more control, you could make your own version of FQuantizedPlayCommand (inherit from IQuartzQuantizedCommand) and override OverrideFramesUntilExec() to decide what to do after Quartz has determined the deadline for the event.