Funscript to mp3 for Dungeon Labs Coyote 3

import json
import numpy as np
from pydub import AudioSegment
from scipy.io.wavfile import write
import argparse
import os

def generate_pulse_train(prev_pos, current_pos, delta_time, sample_rate=1000, frequency=100):
    # Pulse levels (normalized to 1.0 scale)
    pulse_levels = np.array([0.35, 0.50, 0.65, 0.80, 1.00, 0.50, 0.00])
    
    # Calculate the maximum amplitude based on the inversion difference
    amplitude = abs(current_pos - prev_pos) / 100.0
    scaled_levels = pulse_levels * amplitude
    
    # Determine the duration of each pulse segment
    segment_duration = delta_time / 7
    segment_samples = int(segment_duration * sample_rate)
    
    # Generate the pulse train
    pulse_train = []
    for level in scaled_levels:
        t = np.linspace(0, segment_duration, segment_samples, False)
        sine_wave = level * np.sin(2 * np.pi * frequency * t)
        pulse_train.extend(sine_wave)
    
    return np.array(pulse_train)

def funscript_to_audio(funscript, output_file):
    # Parameters
    sample_rate = 1000  # 1kHz sampling rate
    
    # Parse the Funscript
    actions = funscript['actions']
    
    # Create an empty audio array
    total_duration = actions[-1]['at'] / 1000  # convert from ms to seconds
    audio = np.zeros(int(sample_rate * total_duration))
    
    # Initialize inversion detection variables
    inversion_count = 0
    amplitude_values = []
    
    # Track the last two positions
    if len(actions) < 3:
        print("Not enough actions to detect inversions.")
        return
    
    prev_pos = actions[0]['pos']
    prev_prev_pos = actions[1]['pos']
    prev_time = actions[1]['at'] / 1000  # convert ms to seconds
    
    for i in range(2, len(actions)):
        current_pos = actions[i]['pos']
        current_time = actions[i]['at'] / 1000  # convert ms to seconds
        
        # Check if an inversion has occurred (compare current, previous, and previous-previous pos)
        if (prev_prev_pos < prev_pos > current_pos) or (prev_prev_pos > prev_pos < current_pos):
            # Inversion detected, generate pulse train
            delta_time = current_time - prev_time
            pulse_train = generate_pulse_train(prev_pos, current_pos, delta_time, sample_rate)
            
            # Determine where to place the pulse train in the audio array
            start_sample = int(prev_time * sample_rate)
            end_sample = start_sample + len(pulse_train)
            
            # Add the pulse train to the audio track
            audio[start_sample:end_sample] += pulse_train
            
            # Update stats
            inversion_count += 1
            amplitude_values.append(abs(current_pos - prev_pos) / 100.0)
            
            # Update previous inversion point
            prev_prev_pos = prev_pos
            prev_pos = current_pos
            prev_time = current_time
        else:
            # Update tracking without generating a pulse train
            prev_prev_pos = prev_pos
            prev_pos = current_pos

    # Normalize the audio to prevent clipping
    max_value = np.max(np.abs(audio))
    if max_value > 0:
        audio = np.int16(audio / max_value * 32767)
    else:
        audio = np.int16(audio)  # No normalization needed if max_value is 0
    
    # Write the audio to a WAV file
    wav_file = "output.wav"
    write(wav_file, sample_rate, audio)
    
    # Convert to MP3 using pydub
    sound = AudioSegment.from_wav(wav_file)
    sound.export(output_file, format="mp3")
    print(f"Saved {output_file}")
    
    # Print stats
    print(f"Total duration: {total_duration:.2f} seconds")
    print(f"Number of inversions detected: {inversion_count}")
    if amplitude_values:
        print(f"Amplitude range: {min(amplitude_values):.2f} to {max(amplitude_values):.2f}")

def main():
    # Argument parser setup
    parser = argparse.ArgumentParser(description="Convert Funscript to MP3 with Pulse Train")
    parser.add_argument("funscript", type=str, help="Path to the Funscript JSON file")

    # Parse arguments
    args = parser.parse_args()

    # Extract the filename without extension and create the output filename
    input_file = args.funscript
    output_file = os.path.splitext(input_file)[0] + ".mp3"

    # Load the Funscript file
    with open(input_file, 'r') as f:
        funscript = json.load(f)

    # Convert the Funscript to an MP3 file with pulse train
    funscript_to_audio(funscript, output_file)

if __name__ == "__main__":
    main()

This Python script converts a Funscript file into an MP3 audio file. The script detects inversion points in the Funscript’s pos values and generates a corresponding pulse train. The pulse train is modulated based on the amplitude differences between inversion points, with a fixed sine wave frequency of 100 Hz. The final audio provides a dynamic representation of the motion described in the Funscript.

Install prerequisites : pip install numpy scipy pydub

Usage : python3 funscript_to_mp3.py "your_funscript.funscript"

1 Like