Skip to content

The funscript format, as a spec

A funscript is a UTF-8 JSON file, ending in .funscript, that lists times and positions so a connected device can move in time with a video or audio file. This page writes the format down as a reference. It describes what scripts and players already do, and adds recommendations where practice varies; it does not invent a new format.

The words must, should and may carry their usual meaning in specifications. “Writer” means any tool or person producing a script; “player” means software that reads one and drives a device.

  • A script pairs with one media file and shares its name: evening.mp4 and evening.funscript.
  • Players should pair files in the same folder by name, ignoring the extensions.
  • Extra axes use additional files (see multi-axis).

The file is one JSON object. Only actions is required.

{
"version": "1.0",
"inverted": false,
"range": 100,
"metadata": { "title": "Evening", "creator": "Your name", "license": "CC BY 4.0" },
"actions": [
{ "at": 0, "pos": 10 },
{ "at": 400, "pos": 90 },
{ "at": 650, "pos": 30 },
{ "at": 1000, "pos": 85 },
{ "at": 1500, "pos": 10 }
]
}
Field Type Required Meaning
actions array of action yes The points to play, in time order
actions[].at number, ms, ≥ 0 yes Time from the start of the media
actions[].pos number, 0–100 yes Position: 0 is the bottom of the stroke, 100 the top
version string no Format version, usually "1.0"
inverted boolean no When true, 0 is the top and 100 the bottom
range number, 0–100 no The stroke range the script was written for
metadata object no Editor details: title, creator, description, license, tags, duration and others

Unknown fields must be ignored by players and should be preserved by editors.

  • write at and pos as whole numbers;
  • sort actions by at, with no two points at the same time;
  • keep pos within 0–100;
  • leave inverted false and range at 100 unless the script truly needs them.
  • accept unsorted actions by sorting them, and keep only the last point when two share a time;
  • clip positions outside 0–100 to the nearest end;
  • skip points that lack a number for at or pos, and tell the user;
  • honour inverted by mapping every position to 100 - pos.

Between two consecutive points the device moves in a straight line. Speed is distance over time: from position 10 at 0 ms to position 90 at 400 ms is 80 units in 0.4 s, or 200 units per second. Before the first point a player holds the first position; after the last point, the last.

Positions are relative to the range the user sets for the device. A player may let the user narrow that range (ANANGA calls this the user’s fit) and should map 0–100 onto it.

Real devices have a top speed and need a moment between commands. ANANGA recommends these defaults, and uses them in its player and library:

Limit Default What a player should do
Fastest move 500 units per second Shorten the move so it stays under the limit, keeping its timing
Smallest gap between points 80 ms Merge points that come closer, keeping the extremes

Slower devices need lower limits. A player should not silently skip moves, because skipped strokes break sync with the media.

Devices such as the OSR2 and SR6 also rotate and tilt. Each extra axis lives in its own file, named after the main script with the axis before .funscript. The names come from the TCode protocol these devices use.

Axis TCode Motion File name
Stroke L0 Up and down, the main axis evening.funscript
Surge L1 Forward and back evening.surge.funscript
Sway L2 Left and right evening.sway.funscript
Twist R0 Rotation evening.twist.funscript
Roll R1 Tilt left and right evening.roll.funscript
Pitch R2 Tilt forward and back evening.pitch.funscript

Players should also accept the channel code as the name (evening.R0.funscript). TCode defines further channels for vibration (V0), valve (A0) and suction (A1) on devices that have them. Some editors store several axes in one file under an axes array of { "id": "R0", "actions": [...] }; players may read it.