MultiFunPlayer v1.32.0 - Multi axis funscript player - Now with SLR script streaming

Thank you for the constant updates and improvements!!! Hardest working programmer that we got. Ya’ll need to join Yoooi’s Patreon!

2 Likes

it is done. :smiley: :smiley: :smiley:

I connected it to MPV and buttplug. moves fine in buttplug, but doesn’t move at all when the script is functioning and moving the bars.

Did you map your devices in buttplug.io settings in MultiFunPlayer?

thanks, this needs to be in the How to section too

Is there anyway to export instances of the interpolated data? I’m looking for a solution to my question over at:

TBH I dont know what exactly do you mean by “interpolated”, I didnt know in that post too, but I will try to add a feature to only enable motion providers when there is a gap.
There is no way to export axis motion data at the moment, but ability to record to a file is not a bad idea.

1 Like

Perhaps I am mistaken on what is occurring. I am simply referring to other Axes inferring movement based upon L0. If it’s randomized in movement; But not Speed it’s still a sort of interpolation imo.

If you link axes its basically a copy of the script.
If you add motion provider to an axis with a script you basically have two “tracks”, one is the script, the second one is the motion generated from motion provider. Then those two tracks are blended/mixes together based on the blend slider to produce the output value, which is I think what you call interpolation?

I call it “blending” since they are separate tracks, kinda like you blend two images together. It technically does use linear interpolation under the hood, but I would call it “interpolation” when interpolating between two points in the same data set, for example the interpolation of script points to make smooth curves with something like pchip/makima or interpolating between two pixels on an image.

But it doesn’t matter, just different words with similar meaning, I just couldn’t perfectly understand what you meant.

1 Like

Hi. I use DeoVR with MFP v1.20.2.0 and a TCode device controlled directly from MFP. I noticed that the syncing is inaccurate, the device regularly responds too late or too early. I verified with a camera that the sync seems to oscillate between 300ms ahead and 300ms behind with a period of about 20 seconds.

I looked at wireshark data, comparing the packet time with the “current Time” field in the json api, the jitter is only ~30ms max, which doesn’t explain this problem.

With OpenFunScripter, the device follows the script movements accurately (also verified with camera). MFP + MPC-HC on the same computer also works fine. I tested with low and high serial refresh rates.

In DeoVRVideoSourceViewModel.cs the code attempts to read the propery “currentTime”, but the DeoVR API mixes between “current Time” and “currentTime”. Perhaps this is part of the problem?

1 Like

So any other player works correctly, only DeoVR has this issue?
Are the “Axis Values” bars also out of sync?
Such time difference shouldnt really happen unless playback rate gets out of sync.

Never had the api return “current Time”, are you sure its not just wireshark text formatting?
If you switch log level to trace you should see Received VideoPositionMessage messages every 1 second.

I did some more digging…

I setup a video camera with the following elements visible:
MPC-HC playing a video of a metronome.
DeoVR playing the same video.
MFP pointed at MPC-HC with the axis value clearly visible, interpolation Step
MFP pointed at DroVR with the axis value clearly visible, interpolation Step.

I created a simple funscript and recorded a video (60fps) of this setup. I carefully inspected the video writing down how many frames ahead/behind each thing was relative to MPC-HC, once per second.

The orange line shows that DeoVR plays the video at a slightly slower rate than MPC-HC. We can probably ignore this here.
The Grey line shows that MFP (deovr) axis value moves between -8 frames earlier to +15 frames later than expected (-130ms, +250ms).
The orange line shows that MFP (MPC-HC) axis value moves between -1 frames earlier and +8 frames later than expected (-20ms, +130ms).

Both DeoVR and MPC-HC show a very clear sine wave.

I also checked the logs, everything looks normal. If I compare the log timestamp with the VideoPositionMessage, DeoVR has a jitter <30ms with no noticeable pattern or drift, MPC-HC has a jitter of <60ms with no noticeable pattern but a small drift (0.06s over 60 seconds, probably 30/29.97fps crap).

I suspect that:
OFP does some sort of signal filtering that results in unstable behavior.
MPC-HC sends 5 position updates per second and DeoVR only 1 per second. As a result the unstable behavior is less bad with MPC-HC.

This code looks like a prime candidate for causing the observed behavior:

ScriptViewModel.cs
        var error = float.IsFinite(CurrentPosition) ? newPosition - CurrentPosition : 0;
        _playbackSpeedCorrection = MathUtils.Clamp(_playbackSpeedCorrection + error * 0.1f, 0.9f, 1.1f);

That sure looks unstable.

You’re right, my bad.

2 Likes

Thanks for a nice analysis!
I guess thats what happens when I only test with MPV…

Yup, basically a full PID will be needed.
Added this issue to github.

Actually knowing that each video player has different update rate PID will not work well.
So for now I dont have any good ideas on how to solve that properly.

The solution depends on the problem you’re trying to solve.

If you think the API is misreporting the playback speed, then calculate the playback speed based on timing information in the packets: (last_packet_position - new_position) / (last_packet_timestamp - new_packet_timestamp) plus an exponential filter, this would eliminate any feedback loops. Did you observe this to be a problem? I know many players are off by 1000/1001 due to 30/29.97fps crap, but that really shouldn’t be noticeable.

If you think the packets have a large amount of jitter or the player stutters, then it’s a good idea to keep the playback speed at 1 and micro-adjust the position instead. For example:

    float expectedPosition = currentPosition;
    float error = newPosition - expectedPosition;
    UpdateCurrentPosition(expectedPosition + error * .1);

This should work well if the packet jitter is below 100ms.

If that doesn’t work I have a few more ideas, but they take a bit more effort to implement.


There appears to be some timer creep in this code:

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        float ElapsedSeconds() => stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;

        stopwatch.Start();
        while (!token.IsCancellationRequested)
        {
            ... update

            stopwatch.Restart();
            Thread.Sleep(IsPlaying || dirty ? 2 : 10);
        }

I suggest this modification:

        float elapsedSeconds = 0;
        long lastUpdateTicks = 0;

        stopwatch.Start();
        while (!token.IsCancellationRequested)
        {
            long currentTicks = stopwatch.ElapsedTicks;
            elapsedSeconds = (lastUpdateTicks - currentTicks) / (float)stopwatch.Frequency;
            lastUpdateTicks = currentTicks;

            ... update

            Thread.Sleep(IsPlaying || dirty ? 2 : 10);
        }

Don’t reset the stopwatch. Beware that elapsedSeconds might be zero. Don’t divide by zero when calculating the speed :wink:

The playback speed and position from video players should be more or less fine, its just that there will be processing delays after the video player sends its current position which needs to be compensated.

IIRC I did try something like that but the smoothing was causing slow convergence with DeoVR/HereSphere since it only updates every second. But might have to try this again.

That will work as thats how it more or less was before but since the position is received from a different thread, if there is a negative error then the time can jump back causing micro stutters. The playback speed correction was a fix for that. Script time has to always move forward.

Actually stopwatch reset is pretty much free, i measured about 10ms difference over 60seconds for stopwatch always running vs stopwatch reset and accumulate elapsed ticks.

If we assume the playback speed is accurate then there is not much point in doing arithmetic with packet timestamps.

In that case, modify:

_playbackSpeedCorrection = MathUtils.Clamp(_playbackSpeedCorrection + error * 0.1f, 0.9f, 1.1f);

to:

_playbackSpeedCorrection = MathUtils.Clamp(1 + error * 0.1f, 0.9f, 1.1f);

I don’t think there is much point in accumulating the error over multiple packets if we’re just trying to correct for a little bit of network jitter. The accumulating part makes it difficult to get stable over a wide range of conditions. It’s okay if it takes a few seconds to converge as long as the algorithm is stable.

I was concerned with ticks getting lost between calls to ElapsedSeconds() and stopwatch.reset(). I’ve worked on some time-critical code and these small things really make it a lot harder to debug issues.

I did some additional tests. It appears that with MPC-HC, the initial VideoPositionMessage is quite jank, often being early by ~100ms. It appears that MPC-HC sends the VideoPositionMessage as soon as the user presses the play button, but the player itself needs ~100ms to actually start playing.

The same problem happens with deoVR, when the user presses the play button the API immediately sends the play signal, but the player itself buffers for ~1s.

This results in poor performance with my suggested code, my hypothesis that we’re just correcting for network jank isn’t correct. I’ll spend some time investigating further and report back with a patch that works well under these circumstances…

@GoonerScriptz added File output target so you can save funscripts of motion generated by MFP, and added Fill gaps option to motion providers
@galw86 changed offset sliders to numeric input so the offset range is now infinite

Core code had to change a bit to support the gap fills so hopefully I didnt break much. Please report any issues you encounter.

https://nightly.link/Yoooi0/MultiFunPlayer/actions/runs/2367864698

1 Like

I’m trying to use controller to add shortcuts and only about half the buttons are able to be captured. Also, the majority of the buttons I’m able to capture are of a yellow controller which barely have any actions. Is this intended?