[Release] Restim funscript generator with volume control

Webteases are great, but what if you're in the mood for a slightly more immersive experience? Chat about Tease AI and other offline tease software.

Moderator: 1885

Post Reply
itch
Curious Newbie
Curious Newbie
Posts: 4
Joined: Mon Nov 25, 2024 1:28 pm

[Release] Restim funscript generator with volume control

Post by itch »

I've been on and off Milovana for some time now, but here is the place I learned about estim, so I've decided to contribute. This is kind of extended repost from the milovana subreddit:

I’ve always had an issue with funscript to estim conversion tools, because the files generated with them never felt “right”. Meaning - more often than not, very slow strokes felt too intense for what it should feel, and fast strokes - not intense enough.
With a bit of trial and error, and with the help of chatgpt, I created a piece of software, that creates an additional funscripts to control volume and frequency in Restim.

This is what it does:
1. It creates .alpha and .beta files (this portion of the code is taken directly from Restim).
2. It creates .volume file based on the current movement speed between two timecodes. After some trial and error, I found out it's best to compare the speed against arbitrary values instead of minimum and maximum speed with each script. Meaning - a "slow" funscript will have significantly lower volume than a "fast" one. This is deliberate.
3. Lastly, it creates .frequency funscript. This part is a little experimental. The idea is, that if the current movement speed exceeds the arbitrary top speed (so the volume is at 100 and can't increase further), the frequency starts to drop. In theory, lower frequencies should feel more intense, further enhancing the sensation scale.
4. The software goes through ALL the funscripts in its current directory (excluding .alpha, .beta, .volume, and .frequency) to create the new files. Batch conversion is something I've always been missing in the "Convert 1d to 2d" function of Restim.

----

MOST IMPORTANTLY:
Feel free to download, share and modify the script, but DON’T ASK ME FOR ANY CHANGES. I don’t know python, I’m not even a programmer and the whole thing has been created by chatgpt. I've only tested it, it seems to work fine.

DOWNLOAD LINK:
If you wanted to change anything and recompile the thing, the python source code is also included in the file:
https://mega.nz/file/DmgiSL5Z#XniBkY9bH ... 82QovrheVE

----

Copy-paste source code if the mega link goes down one day.

Code: Select all

import os
import json
import numpy as np

# Constants for speed ranges (from restim_volume)
VOLUME_MIN_SPEED = 0.04
VOLUME_MAX_SPEED = 1.25
VOLUME_MIN_POS = 60
VOLUME_MAX_POS = 100

FREQUENCY_MIN_SPEED = 1.25
FREQUENCY_MAX_SPEED = 25
FREQUENCY_MIN_POS = 0
FREQUENCY_MAX_POS = 100

NON_MOVEMENT_MAX_DURATION = 1000  # 1 second (ms)
NON_MOVEMENT_MAX_POS = 30
NON_MOVEMENT_MIN_POS = 0


def scale_speed_to_pos(speed, min_speed, max_speed, min_pos, max_pos):
    """
    Scale a speed value to a position within the given range.
    """
    if speed <= min_speed:
        return min_pos
    if speed >= max_speed:
        return max_pos
    # Linear interpolation
    return int(min_pos + ((speed - min_speed) / (max_speed - min_speed)) * (max_pos - min_pos))


def handle_non_movement(actions):
    """
    Handle non-movement periods and assign appropriate pos values (0–30) based on duration.
    """
    non_movement_data = []
    for i in range(len(actions) - 1):
        if actions[i]['pos'] == actions[i + 1]['pos']:  # Non-movement detected
            start = actions[i]['at']
            end = actions[i + 1]['at']
            duration = end - start

            if duration >= NON_MOVEMENT_MAX_DURATION:
                assigned_pos = NON_MOVEMENT_MIN_POS
            else:
                assigned_pos = int(NON_MOVEMENT_MAX_POS * (1 - duration / NON_MOVEMENT_MAX_DURATION))

            non_movement_data.append({'at': start, 'pos': assigned_pos})
            non_movement_data.append({'at': end, 'pos': assigned_pos})

    return non_movement_data


def create_scaled_file(actions, min_speed, max_speed, min_pos, max_pos, invert=False, output_path=None, handle_non_movement_flag=False):
    """
    Create a scaled output file based on the speed range and output scaling.
    Optionally handles non-movement.
    """
    scaled_data = []

    if handle_non_movement_flag:
        # Add non-movement data
        scaled_data.extend(handle_non_movement(actions))

    for i in range(len(actions) - 1):
        time_diff = actions[i + 1]['at'] - actions[i]['at']
        pos_diff = abs(actions[i + 1]['pos'] - actions[i]['pos'])
        if actions[i]['pos'] != actions[i + 1]['pos']:  # Movement detected
            speed = pos_diff / time_diff if time_diff > 0 else 0
            scaled_pos = scale_speed_to_pos(speed, min_speed, max_speed, min_pos, max_pos)
            if invert:
                scaled_pos = max_pos - scaled_pos
            avg_at = (actions[i]['at'] + actions[i + 1]['at']) // 2
            scaled_data.append({'at': avg_at, 'pos': scaled_pos})

    scaled_data.sort(key=lambda x: x['at'])
    if output_path:
        with open(output_path, 'w') as f:
            json.dump({'actions': scaled_data}, f, indent=4)


def parse_funscript(path):
    """
    Parse a .funscript file and extract time and position values.
    """
    x = []
    y = []
    with open(path, 'r') as f:
        js = json.load(f)
        for action in js['actions']:
            at = float(action['at']) / 1000
            pos = float(action['pos']) * 0.01
            x.append(at)
            y.append(pos)
    return x, y


def write_funscript(path, funscript):
    """
    Write a .funscript file from time and position values.
    """
    x, y = funscript
    actions = [{"at": int(at * 1000), "pos": int(pos * 100)} for at, pos in zip(x, y)]
    js = {"actions": actions}
    with open(path, 'w') as f:
        json.dump(js, f, indent=4)


def convert_funscript_radial(funscript):
    """
    Convert a 1D .funscript into radial (2D) data.
    """
    at, pos = funscript

    t_out = []
    x_out = []
    y_out = []

    for i in range(len(pos) - 1):
        start_t, end_t = at[i:i + 2]
        start_p, end_p = pos[i:i + 2]

        points_per_second = 25
        n = int(np.clip(float((end_t - start_t) * points_per_second), 1, None))
        t = np.linspace(0.0, end_t - start_t, n, endpoint=False)
        theta = np.linspace(0, np.pi, n, endpoint=False)
        center = (end_p + start_p) / 2
        r = (start_p - end_p) / 2

        x = center + r * np.cos(theta)
        y = r * np.sin(theta) + 0.5
        t_out += list(t + start_t)
        x_out += list(x)
        y_out += list(y)

    return t_out, x_out, y_out


def process_funscript(file_path):
    """
    Process a single funscript file to create volume, frequency, alpha, and beta outputs.
    """
    with open(file_path, 'r', encoding='utf-8') as f:
        script_data = json.load(f)

    actions = script_data.get('actions', [])
    if not actions:
        print(f"No 'actions' found in {file_path}. Skipping.")
        return

    # Generate .volume.funscript
    volume_output_path = file_path.replace('.funscript', '.volume.funscript')
    create_scaled_file(
        actions,
        VOLUME_MIN_SPEED,
        VOLUME_MAX_SPEED,
        VOLUME_MIN_POS,
        VOLUME_MAX_POS,
        invert=False,
        output_path=volume_output_path,
        handle_non_movement_flag=True
    )

    # Generate .frequency.funscript
    frequency_output_path = file_path.replace('.funscript', '.frequency.funscript')
    create_scaled_file(
        actions,
        FREQUENCY_MIN_SPEED,
        FREQUENCY_MAX_SPEED,
        FREQUENCY_MIN_POS,
        FREQUENCY_MAX_POS,
        invert=True,
        output_path=frequency_output_path
    )

    # Generate .alpha and .beta files
    funscript = parse_funscript(file_path)
    alpha_output_path = file_path.replace('.funscript', '.alpha.funscript')
    beta_output_path = file_path.replace('.funscript', '.beta.funscript')
    a, b, c = convert_funscript_radial(funscript)
    write_funscript(alpha_output_path, (a, b))
    write_funscript(beta_output_path, (a, c))


def batch_process_folder():
    """
    Process all .funscript files in the current directory, excluding generated outputs.
    """
    current_folder = os.getcwd()
    for file_name in os.listdir(current_folder):
        if file_name.endswith('.funscript') and not any(
            suffix in file_name for suffix in ['.volume.funscript', '.frequency.funscript', '.alpha.funscript', '.beta.funscript']
        ):
            file_path = os.path.join(current_folder, file_name)
            print(f"Processing: {file_path}")
            process_funscript(file_path)


if __name__ == "__main__":
    batch_process_folder()
Last edited by itch on Fri Nov 29, 2024 1:04 pm, edited 2 times in total.
User avatar
edger477
Experimentor
Experimentor
Posts: 1114
Joined: Mon Nov 29, 2021 8:24 pm
Location: underfloor

Re: [Release] Restim funscript generator with volume control

Post by edger477 »

Heh, I did not know other people do this... I'll share script I am using to convert stroke speed to its own funscript, but I am not using this as a volume, this is more of a tool to find fastest parts of stroking and adjust if needed, then I use other tool to help me create volume.

Here is script that converts stroking to speed:

Code: Select all

from funscript.funscript import Funscript
import numpy as np
import argparse




def calculate_speed(funscript):
    """Calculate the absolute speed of change between consecutive points."""
    x = []
    y = []
    max_speed = 0

    # Iterate over consecutive points
    for i in range(1, len(funscript.x)):
        time_diff = (funscript.x[i] - funscript.x[i - 1]) * 1000  # Time difference in milliseconds
        pos_diff = abs(funscript.y[i] - funscript.y[i - 1])  # Absolute position change

        # Avoid division by zero if time_diff is zero
        if time_diff != 0:
            speed = pos_diff / time_diff  # Speed (change per millisecond)
        else:
            speed = 0

        if speed > max_speed:
            max_speed = speed




        # Append the new action with speed as the 'pos' value and original timestamp
        x.append((funscript.x[i]))
        y.append(speed)

    factor = 1 / max_speed
    for i in range(1, len(y)):
        y[i] = y[i] * factor
    return Funscript(x, y)


def calculate_speed_windowed(funscript):
    """Calculate the rolling average speed of change over the last n seconds."""
    x = []
    y = []
    max_speed = 0
    time_window = 10  # n=10 seconds

    # Iterate over each point
    for i in range(1, len(funscript.x)):
        current_time = funscript.x[i - 1]
        pos_current = funscript.y[i]

        # Initialize variables for rolling sum
        total_speed = 0
        count = 0

        # Look back at all points within the last n seconds
        for j in range(i, -1, -1):
            if current_time - funscript.x[j] > time_window:
                break  # Stop if we're outside the n-second window

            time_diff = funscript.x[j] - funscript.x[j-1]  # Time difference in milliseconds
            pos_diff = abs(funscript.y[j] - funscript.y[j-1])  # Absolute position change

            # Avoid division by zero if time_diff is zero
            if time_diff != 0:
                speed = pos_diff / time_diff  # Speed (change per millisecond)
                total_speed += speed
                count += 1

        # Calculate the average speed over the rolling window
        avg_speed = (total_speed / count) if count > 0 else 0

        if avg_speed > max_speed:
            max_speed = avg_speed

        # Append the new action with average speed as the 'pos' value

        x.append((funscript.x[i]))
        y.append(avg_speed)

    factor = 1 / max_speed
    for i in range(len(y)):
        y[i] = y[i] * factor

    return Funscript(x, y)




def main(funscript_file, output_file):
    # Load the funscript files
    funscript = Funscript.from_file(funscript_file)

    # Combine the funscripts
    speed = calculate_speed_windowed(funscript)

    # Save the combined funscript
    speed.save_to_path(output_file)
    print(f"Speed Funscript saved to {output_file}")


if __name__ == "__main__":
    # Set up argument parsing
    parser = argparse.ArgumentParser(description="Convert speed of input funscript to output.")
    # parser.add_argument('funscript_file1', type=str, help="The path to the first input Funscript file.")
    # parser.add_argument('funscript_file2', type=str, help="The path to the second input Funscript file.")
    # parser.add_argument('output_file', type=str, help="The path to save the combined Funscript file.")
    parser.add_argument('filename', type=str,
                        help="The input file name. Output is filename.speed.funscript")

    # Parse the arguments
    args = parser.parse_args()

    # Call the main function with the provided arguments
    # main(args.funscript_file1, args.funscript_file2, args.output_file)
    main(args.filename + ".funscript", args.filename + ".speed.funscript")

I use this speed as part of the volume, I make another one that is "ramp" script (starts low, ends high, and I add to that one any "drops" (i.e. most intense climaxes of the video should be peaks here, then volume dropping a bit afterwards), and it combines "volume_ramp" with "speed" by using 3/4ths of ramp and only 1/4th of the speed (you can adjust that in script for different effects)

Code: Select all

from funscript.funscript import Funscript
import numpy as np
import argparse

def multiply_funscripts(left: Funscript, right: Funscript) -> Funscript:
    x = np.union1d(left.x, right.x)
    y = np.interp(x, left.x, left.y) * np.interp(x, right.x, right.y)
    return Funscript(x, y)


def avg_funscripts(left: Funscript, right: Funscript) -> Funscript:
    x = np.union1d(left.x, right.x)
    y = (np.interp(x, left.x, left.y) * 3 + np.interp(x, right.x, right.y)) / 4
    return Funscript(x, y)



def main(funscript_file1, funscript_file2, output_file):
    # Load the funscript files
    funscript1 = Funscript.from_file(funscript_file1)
    funscript2 = Funscript.from_file(funscript_file2)

    # Combine the funscripts
    combined_actions = avg_funscripts(funscript1, funscript2)

    # Save the combined funscript
    combined_actions.save_to_path(output_file)
    print(f"Combined Funscript saved to {output_file}")

if __name__ == "__main__":
    # Set up argument parsing
    parser = argparse.ArgumentParser(description="Combine two Funscript files by multiplying their position values.")
    #parser.add_argument('funscript_file1', type=str, help="The path to the first input Funscript file.")
    #parser.add_argument('funscript_file2', type=str, help="The path to the second input Funscript file.")
    #parser.add_argument('output_file', type=str, help="The path to save the combined Funscript file.")
    parser.add_argument('filename', type=str, help="The file name. We combine filename.volume_ramp.funscript with filename.speed.funscript into filename.volume.funscript")
    
    # Parse the arguments
    args = parser.parse_args()

    # Call the main function with the provided arguments
    #main(args.funscript_file1, args.funscript_file2, args.output_file)
    main(args.filename + ".volume_ramp.funscript", args.filename + ".speed.funscript", args.filename + ".volume.funscript")
These are tools that I have been using to create some of my recent works.
My estim creations: https://mega.nz/folder/73pxmBBQ#X6ylDzRafzTt9wanZ0dacw
And in E-Stim Index: viewtopic.php?t=27090

Try creating your own estims with my restim script generator!
Spoiler: show
You can also thank me with crypto: https://trocador.app/anonpay?ticker_to= ... e+a+coffee
itch
Curious Newbie
Curious Newbie
Posts: 4
Joined: Mon Nov 25, 2024 1:28 pm

Re: [Release] Restim funscript generator with volume control

Post by itch »

edger477 wrote: Mon Nov 25, 2024 3:26 pm Heh, I did not know other people do this...
Yeah, I was thinking about it for a long time, but only recently I realized I could do thing without refreshing everything I used to know about java nor by learning python. These AI tools are great, IF you know what you want to achieve.
thebears73
Explorer At Heart
Explorer At Heart
Posts: 348
Joined: Sat May 11, 2019 7:22 am

Re: [Release] Restim funscript generator with volume control

Post by thebears73 »

tried to download and says an error when extracting the file. also not liking the .7z file, do you have a zip file i can try please?
itch
Curious Newbie
Curious Newbie
Posts: 4
Joined: Mon Nov 25, 2024 1:28 pm

Re: [Release] Restim funscript generator with volume control

Post by itch »

I checked and the archive is okay, so it must be something on your end. Anyway, since you asked, here's a zipped executable:
https://mega.nz/file/yqInSZLK#SefFrAF22 ... yDwcI13F0U

Btw. I also found that, for some reason, Windows Defender on my computer identifies this files as threats (even though antivirus software doesn't). Go figure :D
User avatar
zmedlnow
Curious Newbie
Curious Newbie
Posts: 2
Joined: Wed Dec 04, 2024 9:59 pm
Gender: Male
Sexual Orientation: Straight

Re: [Release] Restim funscript generator with volume control

Post by zmedlnow »

edger477 wrote: Mon Nov 25, 2024 3:26 pm
Here is script that converts stroking to speed:

Code: Select all

from funscript.funscript import Funscript
I'm assuming that Funscript class is from the restim files?
User avatar
edger477
Experimentor
Experimentor
Posts: 1114
Joined: Mon Nov 29, 2021 8:24 pm
Location: underfloor

Re: [Release] Restim funscript generator with volume control

Post by edger477 »

zmedlnow wrote: Thu Dec 05, 2024 5:26 pm
edger477 wrote: Mon Nov 25, 2024 3:26 pm
Here is script that converts stroking to speed:

Code: Select all

from funscript.funscript import Funscript
I'm assuming that Funscript class is from the restim files?
Correct
My estim creations: https://mega.nz/folder/73pxmBBQ#X6ylDzRafzTt9wanZ0dacw
And in E-Stim Index: viewtopic.php?t=27090

Try creating your own estims with my restim script generator!
Spoiler: show
You can also thank me with crypto: https://trocador.app/anonpay?ticker_to= ... e+a+coffee
Wen74
Explorer
Explorer
Posts: 12
Joined: Mon Oct 04, 2021 6:14 pm

Re: [Release] Restim funscript generator with volume control

Post by Wen74 »

Thank you! That is just brilliant. I was struggling with just the issue (fast ones didn't feel like anything) and the batching is just divine sent.

I'll dig into this and give some feedback. Just wanted to thank you.
Wen74
Explorer
Explorer
Posts: 12
Joined: Mon Oct 04, 2021 6:14 pm

Re: [Release] Restim funscript generator with volume control

Post by Wen74 »

After playing around with the script for a moment I do agree that it shouldn't use the max and min of the script. In the scripts that I've done (basically a pattern based funscript gen) I've noticed that at least for me, stroke frequencies over 120BPM start to muddle up and feel like a continuous signal. Might be my device. Or me. :-)

I did some analysis on my funscripts (1244) and found out the following:
Average speed in the files - 0,31
Median speed in the files - 0,31
Max speed average in the files - 1,27
Max speed median in the files - 0,77

Based on this I tried a few files with:
VOLUME_MIN_SPEED = 0.04
VOLUME_MAX_SPEED = 0.6

That seemed to give nice range without flattening the top end.

For full 0-100 motion 0.6 speed would equate 180BPM for a full stroke. Sounds reasonable. The original value of 1,25 seems real high, even though this would most likely depend on the files used. Of the 1244 I checked, only 267 had max speed over 1,0 but I think we should look more at the average value. There are quite a few outliers (max speed..).

Considering that the average of 0,31 would be ~90BPM for full stroke (or full circle in restim) I think that sounds fine.

In the end these are matters of taste and higher MAX_SPEED will flatten the curve of the volume file.

I tried a few simple changes such as quadratic scaling for the volume and even cubic and this would make the volume peaks more pronounced. Next thing: Playing around with the frequency.

Thanks again for this great toy.
Electro
Explorer At Heart
Explorer At Heart
Posts: 562
Joined: Thu Feb 13, 2020 9:45 am

Re: [Release] Restim funscript generator with volume control

Post by Electro »

Wen74, Calibration and electrode positions matter too. If it's a stroking script where it's 0, 100, 0, 100 full up and down stroke positions, the best thing to use is map to edge and then use an app with a VU meter and watch how the channels behave with the arc position. Certain calibration values can actually make the left channel increase while approaching the right red dot, and vice versa for the left dot. It seems like you'd expect that increasing or decreasing the neutral calibration wouldn't make the left and right positions muddy or cause them to, but it can and it's not intuitive to know when it will produce positions that drop off a cliff when you expect them to still be strong.

Higher frequencies also seem to wipe out the subtle nature of some positions of movement, 863hz works for me, but I find 600hz is better because it gives a deeper feel and if you use pulsing, try using continuous.

****Try these values if you are using a common electrode in a middle position. If you use the common at the head, this information likely isn't useful but then you also probably can actually feel something between the bottom of the left and red dots too.
-.6db neutral, 1db right power, 0db center power, map to edge 260 degree arc with 220 degree arc length.
or
-1.3db neutral, 1db right power, 0db center power, map to edge 260 degree arc with 210 degree arc length.
Try continuous mode with 600hz with the settings above, this part is less important than the calibration values, but it's worth trying all of this.
****

You'll also be turning the volume down significantly versus the pulse mode, usually I need to turn up the volume by about 20% to feel the same intensity with pulse mode versus continuous, especially when pulse mode isn't near full duty cycle.

I'm not kidding when I say that even a .1db difference can make or break positional sensation and if what I'm saying doesn't make sense, try some different values and watch them on a VU meter(I looked at the one in VoiceMeeter while figuring this all out) or look at calibrated outputs in Audacity to see what I'm talking about, you can easily find the left or right channel going down well before their dots or little reduces intensity 'scoops' at the positions you'd expect them to not be doing it. The settings above I've found reduce that effect and make full stroke red-light green-light type stuff and position specific funscripts(BJ, dildo, riding, fucking, and other direct interaction scripts) feel odd, inaccurate, or muddy.
diglet
Explorer At Heart
Explorer At Heart
Posts: 390
Joined: Sun Dec 11, 2022 5:43 pm

Re: [Release] Restim funscript generator with volume control

Post by diglet »

Thanks for all the experimentation. Good to see people like my software enough to build new software on top of it. :-)

I've been thinking about the idea of making the volume dependent on speed for a long time, I discussed it back in 2023 but for some reason I never actually built it :weep: . The idea was to make the estim more closely resemble stroker toys. Stroker toys feel more intense as the strokes-per-second increases, it seems to be the opposite for estim, the slower you go the more intense it feels. The solution discussed in this topic should fix that.


I don't think it's a good idea to mess with the frequency. You can set the frequency however you want of course, but changing the frequency during the session messes with the calibration because skin resistance depends on frequency.
itch
Curious Newbie
Curious Newbie
Posts: 4
Joined: Mon Nov 25, 2024 1:28 pm

Re: [Release] Restim funscript generator with volume control

Post by itch »

Wen74 wrote: Sun Dec 08, 2024 1:01 pm Thanks again for this great toy.
You're welcome :)
diglet wrote: Mon Dec 09, 2024 3:13 pm I don't think it's a good idea to mess with the frequency. You can set the frequency however you want of course, but changing the frequency during the session messes with the calibration because skin resistance depends on frequency.
You might be right. Thruth be told, I didn't really dwell into all that useful information on restim wiki, nor did I spend much time testing and refining this code. Might as well be, that the output volume range is too high anyway (since it goes from 60 all the way up to 100, and then some if you include the frequency change). That's why I figured I'd share it and let everyone else tinker with it, hoping this would spark an idea in some who actually knows what he's doing :D
User avatar
zmedlnow
Curious Newbie
Curious Newbie
Posts: 2
Joined: Wed Dec 04, 2024 9:59 pm
Gender: Male
Sexual Orientation: Straight

Re: [Release] Restim funscript generator with volume control

Post by zmedlnow »

I've been using this for a while with success. My goal was to create volume funscripts that were purely a speed correction factor, nothing more.
My implementation copies the code from the OP, but slightly rearranged to better match the restim Funscript class.
I also added some metadata functionality to the restim Funscript class so the volume scripts generated would have some identifier that they were generated this way (instead of crafted by hand for a more unique effect).

I set the min volume to 0.70, the max volume to 1.0, the upper speed bound at 600units/sec, and the lower speed bound to 0units/sec.
The interpolation I used was linear (copying the OP). It's close enough for me given the typical speed of most scripts, but it's not perfect.
I chose 600u/s because I recall a thread on Eroscripts saying that's about the limit for SR6, and Handy's limit is lower than that. So most scripts won't have a top speed higher than that. Having the speed bounds be fixed made playing different scripts back to back feel consistent.

Between the interpolation method and the volume bounds, there is room for improvement, but I haven't really bothered to squeeze it out since it works well enough as is.

Code snippet if you're curious. It's not much different than what's already posted.
Spoiler: show

Code: Select all

# Volume correction settings
MAX_STROKE_SPEED = 600
MIN_STROKE_SPEED = 0
MAX_VOLUME = 1.0
MIN_VOLUME = 0.70

# corrective speed factor volume script
def correct_volume_from_speed(funscript: Funscript):
    at, pos = funscript.x, funscript.y
    
    t_out = []
    v_out = []
    
    for i in range(len(pos) - 1):
        time_diff = at[i+1] - at[i]
        pos_diff = abs(pos[i+1] - pos[i]) * 100
        
        if pos_diff != 0:
            speed = pos_diff / time_diff if time_diff > 0 else 0
            scaled_pos = scale_speed_to_pos(speed)
            target_at = at[i] 
            
            t_out.append(target_at)
            v_out.append(scaled_pos)
            
    metadata = {"type": "restim speed/volume correction factor"}
    
    return t_out, v_out, metadata
            
def scale_speed_to_pos(speed):
    # set to bounds, or interpolate
    if speed >= MAX_STROKE_SPEED:
        return MAX_VOLUME
    if speed <= MIN_STROKE_SPEED:
        return MIN_VOLUME
    return MIN_VOLUME + ((speed - MIN_STROKE_SPEED) / (MAX_STROKE_SPEED - MIN_STROKE_SPEED)) * (MAX_VOLUME - MIN_VOLUME)
Post Reply