Roon cutting off end of tracks with HQPlayer

Roon should be using playback queue feature of HQPlayer and not issue stop commands or other playlist operations between tracks.

4 Likes

You only get it if you have latency, for example from convolution file with high sample rate and high number of taps. But you could try sinc-M and up-sample to say 192khz and it will give some latency.

I’ve got 5 sec latency because of convolution and filter, but with core and HQPlayer on the same machine the problem is gone.

Roon don’t seem to interested in fixing things like this nowadays, its more about interface tweaks. And where is @brian?

Latency should only be there when you first hit play in Roon (initialisation).

Shouldn’t be there between each track, like if listening to a gapless live album / dj mix.

You don’t get the cut-off between tracks on same album, only between albums or between song with different sample rate or bit depth.

1 Like

I upsample all PCM and DSD content to DSD256 in HQPlayer


So do I. I got the cut end syndrome when I had core and HQPlayer on different machines. Running both on the same machine resolved the problem.

This is incompatible with our architecture. How can we find out that the stream has really ended (from the perspective of the ears of the user)?

HQPlayer reports state has changed to “stopped”. If you ask HQPlayer to stop, it will stop which is expensive operation.

If you want to proceed to next URI without interrupting current one, do “PlaylistAdd” with

queued="1"

attribute set before current one ends. (you have also option for “clear” attribute)

If you want to immediately proceed (seek or user interactively changing track case), also issue “Next” command. There is also

<Play last="1"/>

possible.

Or alternatively use the “PlayNextURI” command.

Extremely long filters mean that head and tail of the filter can add several seconds to beginning and end of playback, compared to formal track length.

P.S. It would be great if Roon would support name based HQPlayer auto-discovery instead of having to fill in IP/hostname (nice option though)
 Very useful when HQPlayer is running on a machine with DHCP assigned IP.

2 Likes

By “name-based”, you mean we should resolve DNS? That’s a simple thing for us to do.

We can’t really use the queueing stuff–this is a common friction point when we set Roon’s transport on top of someone else’s transport, as with HQPlayer, UPnP-like systems, etc. Roon is operating with an abstraction that is more like a stream. The stream may contain several tracks, gapless, crossfaded, etc.

When the format changes, we wait for the stream to play out, then start a new one with the new format. We don’t have the information about the new stream until our state machine advances to that point, after the previous one finished, so promising HQPlayer a URL that may/may not exist in the future is a little bit uncomfortable.

I took a quick look in the code, and it looks like we are trying to determine that the stream ended by catching HQPlayer’s transition to “stopped”. Is there any chance that HQPlayer is transitioning before the audio is done playing from the user’s perspective?

Should be easy to test by playing local files from HQPlayer, my guess is that HQPlayer stops audio when it gets a stop from Roon, instead of letting the “latency time” of music play first.

Having said that, if you press pause/stop in Roon you want the music to stop as soon as possible. So maybe some transition-stop command is needed?

No, you send multicast discovery packet and see who are the HQPlayer’s around. No DNS needed, each HQPlayer instance responds with their name. More like Avahi/ZeroConf/Bonjour. You can use reverse-DNS on the host IP though.

You can see the client API source code for example implementation (that is used by HQPlayer Client and hqp-control2).

This is not a problem, you need a new stream only once the format changes. You don’t need to stop/restart HQPlayer for example when you seek like you now do. If you want faster change, you can use PlayNextURI.

You shouldn’t and you don’t need to, you just put next item on the list and let playback naturally proceed there.

Yes, it is normal thing to happen. At that point you only know that HQPlayer is done reading the source.

Let’s say filter has 10 second long tail, then after last sample of a track still 10 seconds of silence is pushed through the pipeline to get the entire tail out of the filter. At that point, playback is 10 seconds past length of the track.

Yes, that’s the case, because HQPlayer cannot know why the stop was asked. It is assumed user wanted to stop the playback.

HQPlayer will stop by itself once it has played out the source. If you want to cut a stream shorter, you just disconnect HQPlayer’s stream connection. There is no need or point in “transition-stop”.

But isn’t that exactly what HQPlayer doesn’t do, at least not if you factor in latency? Roon does a reset (or something similar because of new sample rate) and HQPlayer cuts the music, which leads to cutting off the end of the track.

Roon asks HQPlayer to stop, clears the playlist, adds new item to the playlist and then asks for playback.

This means HQPlayer will stop, tear down the DSP pipeline, stop the DAC and then start everything all over again. Lot of unnecessary heavy lifting.

Instead of Roon just adding next item to the list and letting HQPlayer to proceed there. Or do that even just when HQPlayer tells it has finished (not as reliable).

Yes, thats why I proposed another command, that will continue playing but with a new song with a new sample rate and/or bit depth. Then Roon can use that between songs when needed, and HQPlayer can reuse whatever code you have for similar scenarios when playing from a playlist.

We are not going to do that.

The problem with giving you the next track early, is that it creates a large window where the user could change what the next track is but we’ve already told HQPlayer something about it.

This is a tricky condition to handle within any media player and requires a fair amount of careful synchronization. There’s no way we could support code attempting to do that in an environment where there is no atomicity or synchronization possible.

Yes, doing a full reset is more heavy-weight in terms of the number of steps, but it’s also safer, and allows for a clean synchronization point where Roon is sure that HQPlayer is doing nothing, and then Roon can atomically (on our end) determine the next track and get the next stream going.

We have tried using proposals like yours in the past with other UPnP-style transport abstractions. It just doesn’t end up working well for the reasons I stated above. It results in a lot of flakey bugs around the edges.

This is the root of the problem. This may be normal for HQPlayer, but it is not normal in general :slight_smile:

The playhead and transport state in a media player should correspond to the presentation clock–what the user is hearing. In computer audio, this is obtained at the point where audio samples are being transmitted to the driver, and comes with side-channel data about additional delays known to the driver that should be handled by the application. Then the application adds knowledge of its own delays.

When HQPlayer says “I am stopped”, Roon is (understandably) assuming that it is actually done playing and that doing the reset procedure for the next stream won’t interrupt anything.

If HQPlayer tells Roon that it is stopped while it’s still playing
that’s going to cause problems like this.

Maybe caching in HQPlayer is the only way to solve this, because if HQPlayer delays reporting stop to Roon to avoid cutting the end of the tune, then the latency will still be there but now between the tunes.

Lets take an example: tune A is 3 min long, its followed by tune B in another sample rate and there is a 10 second latency in HQPlayer due to DSP and filters. After 3 minutes, Roon will send a stop to HQPlayer, but only 2:50 of the tune has actually been playing:

  1. If HQPlayer stops and starts playing tune B at this point, the end of tune A will be cut, and there will still be a 10 second delay before tune B starts
  2. If HQPlayer delays the stop and makes Roon delay the start of next tune, the end of A will not be cut but the latency between tunes will still be there.
  3. If HQPlayer implements caching of the data that is in the pipeline, so that it starts caching tune B from 2:50 while playing the last 10 seconds of A, and then at 3:10 (when the user will hear end of A) it does the reset, there will be no latency anywhere nor will the end of A be cut.

3 is obviously the best, followed by 2 and worst is 1.

You can update that at any time anyway if user changed his mind.

No, it is specifically not atomic because you have multiple commands. All commands sent to HQPlayer are executed atomically, but not any series of commands. So you would be much better off using something like PlayNextURI.

Someone could perform any number of operations between the commands you are sending.

HQPlayer is done playing your content, but don’t make any assumptions about audio output which should not be of your interest. It is HQPlayer’s business what happens once samples are read from the stream.

This is the problem, going to next stream does not require reset procedure. That reset procedure shouldn’t happen, instead you should proceed with the next stream and skip the reset procedure!

This is more like a DAC or any other black-hole sink. When you are using for example I2S and similar outputs, you have stream always going and clocks active, whether there is audio or not. In normal transitions you shouldn’t be stopping clocks and powering down hardware into standby mode.

Consider “Stop” command as equivalent of standby-button in hardware devices.