Python script that converts audio files to funscript file.
import tkinter as tk
from tkinter import filedialog, messagebox
import json
import os
from pydub import AudioSegment
from pydub.utils import make_chunks
# Tooltip class for displaying tips
class CreateToolTip(object):
def __init__(self, widget, text='widget info'):
self.waittime = 500 # milliseconds
self.wraplength = 300 # pixels
self.widget = widget
self.text = text
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", self.leave)
self.id = None
self.tw = None
def enter(self, event=None):
self.schedule()
def leave(self, event=None):
self.unschedule()
self.hidetip()
def schedule(self):
self.unschedule()
self.id = self.widget.after(self.waittime, self.showtip)
def unschedule(self):
id_ = self.id
self.id = None
if id_:
self.widget.after_cancel(id_)
def showtip(self, event=None):
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
# Create toplevel window
self.tw = tk.Toplevel(self.widget)
self.tw.wm_overrideredirect(True) # Removes window decorations
self.tw.wm_geometry(f"+{x}+{y}")
label = tk.Label(self.tw, text=self.text, justify='left',
background="#ffffe0", relief='solid', borderwidth=1,
wraplength=self.wraplength)
label.pack(ipadx=1)
def hidetip(self):
tw = self.tw
self.tw = None
if tw:
tw.destroy()
# Function to analyze audio and create funscript based on volume
def audio_to_funscript(audio_file, output_file, chunk_length_ms, silence_threshold, max_volume_threshold):
# Load audio (supports mp3, wav, etc.)
audio = AudioSegment.from_file(audio_file)
# Convert to mono for easier analysis
audio = audio.set_channels(1)
# Split audio into chunks
chunks = make_chunks(audio, chunk_length_ms)
actions = []
time_ms = 0
for chunk in chunks:
# Calculate loudness in dBFS (decibels relative to full scale)
loudness = chunk.dBFS
# Normalize loudness to range 0-100 for funscript
# Map silence_threshold to 0 and max_volume_threshold to 100
normalized = int(
(loudness - silence_threshold) / (max_volume_threshold - silence_threshold) * 100
)
normalized = max(min(normalized, 100), 0) # Clamp between 0 and 100
# Append action to funscript
actions.append({
"pos": normalized,
"at": time_ms
})
time_ms += chunk_length_ms
# Create funscript structure
funscript_data = {
"version": "1.0",
"inverted": False,
"range": 90,
"info": "Generated from audio volume",
"actions": actions
}
# Write to .funscript file
with open(output_file, 'w') as f:
json.dump(funscript_data, f, indent=4)
# Function to select audio file and generate funscript
def select_file():
# Get parameter values from GUI
try:
chunk_length_ms = int(chunk_length_entry.get())
silence_threshold = float(silence_threshold_entry.get())
max_volume_threshold = float(max_volume_threshold_entry.get())
# Validate chunk length
if not 20 <= chunk_length_ms <= 1000:
messagebox.showerror("Invalid Input", "Chunk length must be between 20 ms and 1000 ms.")
return
# Validate volume thresholds
if silence_threshold >= max_volume_threshold:
messagebox.showerror("Invalid Input", "Silence threshold must be less than Max volume threshold.")
return
except ValueError:
messagebox.showerror("Invalid Input", "Please enter valid numeric values.")
return
# Open file selection dialog
audio_file = filedialog.askopenfilename(
filetypes=[("Audio files", "*.mp3 *.wav")],
title="Select Audio File"
)
if audio_file:
# Generate output file name with .funscript extension
output_file = os.path.splitext(audio_file)[0] + '.funscript'
# Analyze and generate funscript
audio_to_funscript(audio_file, output_file, chunk_length_ms, silence_threshold, max_volume_threshold)
# Notify user
messagebox.showinfo("Done", f"Funscript created at:\n{output_file}")
# GUI Setup
root = tk.Tk()
root.title("Audio to Funscript Converter")
# Instruction label
label = tk.Label(root, text="Select an audio file (MP3 or WAV) to convert to Funscript")
label.pack(pady=10)
# Frame for parameter inputs
params_frame = tk.Frame(root)
params_frame.pack(pady=10)
# Chunk length input
chunk_length_label = tk.Label(params_frame, text="Chunk Length (20-1000 ms):")
chunk_length_label.grid(row=0, column=0, sticky="e", padx=5, pady=5)
chunk_length_entry = tk.Entry(params_frame)
chunk_length_entry.insert(0, "50") # Default value
chunk_length_entry.grid(row=0, column=1, padx=5, pady=5)
CreateToolTip(chunk_length_entry, "Shorter chunks (20-50 ms) capture more detail.\nLonger chunks (200-1000 ms) create smoother actions.")
# Silence threshold input
silence_threshold_label = tk.Label(params_frame, text="Silence Threshold (dBFS):")
silence_threshold_label.grid(row=1, column=0, sticky="e", padx=5, pady=5)
silence_threshold_entry = tk.Entry(params_frame)
silence_threshold_entry.insert(0, "-50") # Default value
silence_threshold_entry.grid(row=1, column=1, padx=5, pady=5)
CreateToolTip(silence_threshold_entry, "Audio below this volume is considered silent.\nRaise this value (-40 to -30 dBFS) for quieter tracks.")
# Max volume threshold input
max_volume_threshold_label = tk.Label(params_frame, text="Max Volume Threshold (dBFS):")
max_volume_threshold_label.grid(row=2, column=0, sticky="e", padx=5, pady=5)
max_volume_threshold_entry = tk.Entry(params_frame)
max_volume_threshold_entry.insert(0, "0") # Default value
max_volume_threshold_entry.grid(row=2, column=1, padx=5, pady=5)
CreateToolTip(max_volume_threshold_entry, "Audio above this level is considered maximum.\nLower this value (-5 to -3 dBFS) to capture more peaks.")
# Button to select audio file
select_button = tk.Button(root, text="Select Audio File", command=select_file)
select_button.pack(pady=20)
# Run the GUI
root.mainloop()