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"