A theory for music to funscript

what is MIDI? midi is a standard old as hell.
It has 2 parts.
On the one hand, there are the midi events that are a port used by dj consoles and instruments. this is used when you connect a piano or dj console to the computer.
in the other part are the mid files that are used to express music in the form of musical notes and instruments. no audio just notes and instruments, midi files are very very light, a few Kb.
Mid File Example: METALLICA.One.mid — Free MIDI — BitMidi
Theoretically, reading the mid files and their different channels (instruments) could retrieve the notes of each channel and its time in the song to generate a movement in the funscript at a certain speed according to the pitch or some other midi property.
since ancient times there are also tools that allow you to convert audio music files to midi files. both in the form of applications and open source libraries.
this technique is used long ago by rhythm games such as audiosurft, ddr and others or Shazam

I personally don’t have the desire or time to do this. but I leave this theory since it seems to me a viable idea if someone wants to try to implement it

reference links:

6 Likes

There’s already MIDI plugins for Unity and Unreal Engine, so adding MIDI to software should be a fairly straightforward thing.

If this turns into a real project, I’d suggest using MIDI 2.0 as a the standard, since it has a lot more resolution and good support for advanced features.

1 Like

The midi file can be converted to an easier to read csv format.

Then it could be converted into a funscript using for example python script.

1 Like

How to Use:

  1. Run the script.
  2. In the GUI window, you’ll see checkboxes arranged in 4 columns, each containing 12 instruments.
  3. Select the desired instruments and click the “Select MIDI file” button.
  4. The script will create a .funscript file with the chosen actions only for the selected instruments and display a confirmation message upon successful export.

instal.bat

pip install mido

mid2funscript.py

import mido
import tkinter as tk
from tkinter import filedialog, messagebox
import os
import json

# Definition of drum instruments with their corresponding MIDI values
drum_instruments = {
    "Acoustic Bass Drum": 35,
    "Bass Drum 1": 36,
    "Side Stick": 37,
    "Acoustic Snare": 38,
    "Hand Clap": 39,
    "Electric Snare": 40,
    "Low Floor Tom": 41,
    "Closed Hi-Hat": 42,
    "High Floor Tom": 43,
    "Pedal Hi-Hat": 44,
    "Low Tom": 45,
    "Open Hi-Hat": 46,
    "Low-Mid Tom": 47,
    "Hi-Mid Tom": 48,
    "Crash Cymbal 1": 49,
    "High Tom": 50,
    "Ride Cymbal 1": 51,
    "Chinese Cymbal": 52,
    "Ride Bell": 53,
    "Tambourine": 54,
    "Splash Cymbal": 55,
    "Cowbell": 56,
    "Crash Cymbal 2": 57,
    "Vibraslap": 58,
    "Ride Cymbal 2": 59,
    "Hi Bongo": 60,
    "Low Bongo": 61,
    "Mute Hi Conga": 62,
    "Open Hi Conga": 63,
    "Low Conga": 64,
    "High Timbale": 65,
    "Low Timbale": 66,
    "High Agogo": 67,
    "Low Agogo": 68,
    "Cabasa": 69,
    "Maracas": 70,
    "Short Whistle": 71,
    "Long Whistle": 72,
    "Short Guiro": 73,
    "Long Guiro": 74,
    "Claves": 75,
    "Hi Wood Block": 76,
    "Low Wood Block": 77,
    "Mute Cuica": 78,
    "Open Cuica": 79,
    "Mute Triangle": 80,
    "Open Triangle": 81,
    "Shaker": 82
}

# Function to export drum notes starts according to user selection
def export_drum_note_starts_to_funscript(midi_file, output_file, selected_instruments):
    mid = mido.MidiFile(midi_file)
    
    actions = []
    cumulative_time = 0
    last_time_ms = None  # Stores the last recorded action time

    for msg in mid:  # Iterate through all events in all tracks
        cumulative_time += msg.time
        
        # Convert time to milliseconds and round
        current_time_ms = round(cumulative_time * 1000)

        # Check if the message has a 'channel' attribute (is not 'MetaMessage') and is on the drum channel
        if msg.type == 'note_on' and hasattr(msg, 'channel') and msg.channel == 9 and msg.velocity > 0:
            # Filter only selected instruments
            if msg.note in selected_instruments:
                # Ignore notes that occur at the same time
                if current_time_ms != last_time_ms:
                    if current_time_ms > 0:
                        # Add action for position 0 (down) and start time only if time > 0
                        actions.append({"pos": 0, "at": current_time_ms - 1})
                    
                    # Add action for position 100 (up) and current start time
                    actions.append({"pos": 100, "at": current_time_ms})

                    # Update the time of the last recorded action
                    last_time_ms = current_time_ms

    # Define JSON content in funscript format
    funscript_data = {
        "version": "1.0",
        "inverted": False,
        "range": 90,
        "info": "Automatic generation script from midi file",
        "actions": actions
    }

    # Save to JSON file with .funscript extension
    with open(output_file, 'w') as f:
        json.dump(funscript_data, f, indent=4)

# Function to select file and export according to user selection
def select_file():
    # Open file selection dialog
    midi_file = filedialog.askopenfilename(
        filetypes=[("MIDI files", "*.mid *.midi")], 
        title="Select MIDI file"
    )
    
    if midi_file:
        # Generate output file name with the same name but .funscript extension
        output_file = os.path.splitext(midi_file)[0] + '.funscript'
        
        # Get selected instruments
        selected_instruments = [
            note for instrument, note in drum_instruments.items() 
            if instrument_vars[instrument].get() == 1
        ]
        
        # Export note times to Funscript format
        export_drum_note_starts_to_funscript(midi_file, output_file, selected_instruments)
        
        # Notify user
        messagebox.showinfo("Done", f"Drum channel note starts have been successfully exported to {output_file}")

# Set up GUI
root = tk.Tk()
root.title("MIDI to Funscript Converter - Drum Channel Only")

# Frame for checkboxes
checkbox_frame = tk.Frame(root)
checkbox_frame.pack(pady=10)

# Variables to store checkbox states
instrument_vars = {}

# Create checkboxes in 4 columns of 12 instruments each
columns = 4
instruments_per_column = 12
instrument_names = list(drum_instruments.keys())

for col in range(columns):
    frame = tk.Frame(checkbox_frame)
    frame.grid(row=0, column=col, padx=10, pady=5)
    
    for row in range(instruments_per_column):
        instrument_index = col * instruments_per_column + row
        if instrument_index < len(instrument_names):
            instrument = instrument_names[instrument_index]
            var = tk.IntVar(value=1 if instrument in ["Acoustic Bass Drum", "Bass Drum 1"] else 0)
            instrument_vars[instrument] = var
            checkbox = tk.Checkbutton(frame, text=instrument, variable=var)
            checkbox.pack(anchor="w")

# Button to select file
select_button = tk.Button(root, text="Select MIDI file", command=select_file)
select_button.pack(pady=20)

# Run GUI application
root.mainloop()

1 Like