Skip to main content
The song system manages song metadata, chart data, audio playback, and the integration between music and gameplay.

Song Architecture

Song Class

The Song class manages song data and metadata:
class Song implements IRegistryEntry<SongMetadata>
{
  public var id:String;                           // Song ID
  public var songName:String;                     // Display name
  public var songArtist:String;                   // Artist name
  public var charter:String;                      // Charter name
  public var variation:String;                    // Current variation
  
  final _metadata:Map<String, SongMetadata>;      // Per-variation metadata
  final difficulties:Map<String, Map<String, SongDifficulty>>; // Chart data
}
Key Properties:
id
String
Unique song identifier (e.g., “tutorial”, “bopeebo”)
songName
String
Display name shown to players
songArtist
String
Artist credit
charter
String
Person who created the chart
variation
String
Current variation (e.g., “default”, “erect”)

Song Metadata Format

Song metadata is stored in assets/data/songs/[id]/[id]-metadata.json:
{
  "version": "2.2.4",
  "songName": "Bopeebo",
  "artist": "Kawai Sprite",
  "charter": "FunkinCrew",
  "divisions": 96,
  "looped": false,
  "offsets": {
    "instrumental": 0,
    "altInstrumentals": {},
    "vocals": {}
  },
  "timeFormat": "ms",
  "timeChanges": [
    {
      "t": 0,
      "bpm": 100,
      "n": 4,
      "d": 4,
      "bt": [4, 4, 4, 4]
    }
  ],
  "playData": {
    "songVariations": [],
    "difficulties": ["easy", "normal", "hard"],
    "characters": {
      "player": "bf",
      "girlfriend": "gf",
      "opponent": "dad",
      "instrumental": "",
      "altInstrumentals": []
    },
    "stage": "mainStage",
    "noteStyle": "funkin",
    "ratings": {
      "easy": 1,
      "normal": 3,
      "hard": 5
    },
    "album": "volume1",
    "previewStart": 0,
    "previewEnd": 15000
  },
  "generatedBy": "FunkinCrew"
}

Metadata Fields

version
String
required
Metadata format version (currently “2.2.4”)
songName
String
required
Display name of the song
artist
String
required
Song artist name
charter
String
Person who charted the song
divisions
Int
default:"96"
Number of divisions per beat (used for chart precision)
looped
Bool
default:"false"
Whether the song should loop
timeFormat
String
default:"ms"
Time format: "ms" (milliseconds), "ticks", or "float"
generatedBy
String
Tool or person that generated this metadata

Time Changes

Songs can have multiple BPM and time signature changes:
{
  "timeChanges": [
    {
      "t": 0,
      "bpm": 100,
      "n": 4,
      "d": 4,
      "bt": [4, 4, 4, 4]
    },
    {
      "t": 32000,
      "bpm": 150,
      "n": 4,
      "d": 4
    }
  ]
}

SongTimeChange Fields

t
Float
required
Timestamp when the change occurs (in the format specified by timeFormat)
bpm
Float
required
New BPM value (quarter notes per minute)
n
Int
default:"4"
Time signature numerator (the ‘4’ in 4/4)
d
Int
default:"4"
Time signature denominator (the ‘4’ in 4/4)
b
Float
Beat time at this change (calculated automatically if not provided)
bt
Array<Int>
default:"[4, 4, 4, 4]"
Beat tuplets - defines step subdivisions for each beat
Example: BPM change mid-song
{
  "timeChanges": [
    {
      "t": 0,
      "bpm": 120,
      "n": 4,
      "d": 4
    },
    {
      "t": 48000,
      "bpm": 180,
      "n": 4,
      "d": 4
    }
  ]
}
This song starts at 120 BPM, then changes to 180 BPM at 48 seconds.

Audio Offsets

Offsets compensate for timing discrepancies:
{
  "offsets": {
    "instrumental": -5.0,
    "altInstrumentals": {
      "remix": -10.0
    },
    "vocals": {
      "bf": 0,
      "dad": 2.5
    }
  }
}

Offset Fields

instrumental
Float
default:"0"
Offset for the main instrumental track (in milliseconds). Negative values start the track earlier.
altInstrumentals
Map<String, Float>
default:"{}"
Offsets for alternate instrumental tracks
vocals
Map<String, Float>
default:"{}"
Per-character vocal offsets (applied on top of instrumental offset)
How offsets work:
  • Negative offset: Audio starts earlier (compensates for delayed chart)
  • Positive offset: Audio starts later (compensates for early chart)
  • Vocal offsets are added to instrumental offset
Example:
{
  "instrumental": -50,
  "vocals": {
    "bf": 10,
    "dad": 0
  }
}
  • Instrumental starts 50ms early
  • BF vocals start 40ms early (-50 + 10)
  • Dad vocals start 50ms early (-50 + 0)

Play Data

The playData section contains gameplay-related metadata:
{
  "playData": {
    "songVariations": [],
    "difficulties": ["easy", "normal", "hard"],
    "characters": {
      "player": "bf",
      "girlfriend": "gf",
      "opponent": "dad"
    },
    "stage": "mainStage",
    "noteStyle": "funkin",
    "ratings": {
      "easy": 1,
      "normal": 3,
      "hard": 5
    },
    "album": "volume1",
    "previewStart": 0,
    "previewEnd": 15000
  }
}

Play Data Fields

songVariations
Array<String>
default:"[]"
List of variations (e.g., ["erect"]). Each variation has its own metadata file.
difficulties
Array<String>
required
Available difficulty levels for this song
characters
SongCharacterData
required
Character IDs for player, opponent, and girlfriend
stage
String
required
Stage ID to use for this song
noteStyle
String
default:"funkin"
Note skin/style ID
ratings
Map<String, Int>
Difficulty ratings shown in freeplay (1-10 scale)
album
String
Album ID for freeplay display
previewStart
Int
default:"0"
Start time for audio preview in freeplay (milliseconds)
previewEnd
Int
default:"15000"
End time for audio preview in freeplay (milliseconds)

Character Data

{
  "characters": {
    "player": "bf",
    "girlfriend": "gf",
    "opponent": "dad",
    "instrumental": "",
    "altInstrumentals": ["remix", "acoustic"],
    "opponentVocals": ["dad"],
    "playerVocals": ["bf"]
  }
}
player
String
default:"bf"
Player character ID
opponent
String
default:"dad"
Opponent character ID
girlfriend
String
default:"gf"
Girlfriend character ID
instrumental
String
default:""
Default instrumental track ID (empty string = default)
altInstrumentals
Array<String>
default:"[]"
List of alternate instrumental IDs
playerVocals
Array<String>
Character IDs whose vocals are in the player track
opponentVocals
Array<String>
Character IDs whose vocals are in the opponent track

Audio File Structure

Audio files are located in assets/songs/[id]/: Required files:
  • Inst.ogg - Main instrumental track
  • Voices-Player.ogg - Player vocals (if song has vocals)
  • Voices-Opponent.ogg - Opponent vocals (if song has vocals)
Optional alternate instrumentals:
  • Inst-[altId].ogg - Alternate instrumental (e.g., Inst-remix.ogg)
File naming conventions:
  • Use .ogg format (Vorbis codec)
  • Capitalize properly: Inst.ogg, not inst.ogg
  • Alternate instrumentals: Inst-[id].ogg
  • Character-specific vocals: Voices-[charId].ogg

Vocal Tracks

Vocals can be split by character or combined: Split by role (recommended):
Voices-Player.ogg
Voices-Opponent.ogg
Split by character:
Voices-bf.ogg
Voices-dad.ogg
Combined vocals (legacy):
Voices.ogg
The engine automatically determines which format is used.

Song Variations

Variations allow alternate versions of a song: Variation structure:
assets/data/songs/bopeebo/
  bopeebo-metadata.json       (default variation)
  bopeebo-chart.json          (default charts)
  bopeebo-erect-metadata.json (erect variation metadata)
  bopeebo-erect-chart.json    (erect variation charts)
Default metadata references variations:
{
  "playData": {
    "songVariations": ["erect"]
  }
}
Each variation has:
  • Separate metadata file
  • Separate chart file
  • Can have different BPM, characters, or stage
  • Can share or have unique audio files

Time Formats

FNF supports three time formats:

Milliseconds (Default)

{
  "timeFormat": "ms",
  "timeChanges": [
    {"t": 0, "bpm": 100},
    {"t": 32000, "bpm": 150}
  ]
}
Times are in milliseconds. Most intuitive for editing.

Ticks

{
  "timeFormat": "ticks",
  "divisions": 96,
  "timeChanges": [
    {"t": 0, "bpm": 100},
    {"t": 3840, "bpm": 150}
  ]
}
Ticks are divisions of a beat. Useful for MIDI imports.
  • 1 beat = divisions ticks (typically 96)
  • Grid-aligned, prevents floating point errors

Float

{
  "timeFormat": "float",
  "timeChanges": [
    {"t": 0, "bpm": 100},
    {"t": 32.0, "bpm": 150}
  ]
}
Times are in seconds as floating point values.

Loading Songs

Songs are loaded via SongRegistry:
// Load song
var song:Song = SongRegistry.instance.fetchEntry("bopeebo");

// Access metadata
trace(song.songName);   // "Bopeebo"
trace(song.songArtist); // "Kawai Sprite"

// Get specific difficulty
var difficulty = song.getDifficulty("hard");
trace(difficulty.getScrollSpeed()); // e.g., 1.5

Conductor Integration

Songs provide timing data to the Conductor:
// Initialize conductor with song time changes
Conductor.instance.mapTimeChanges(song.getTimeChanges());

// During gameplay
Conductor.instance.update(FlxG.sound.music.time);

// Access current timing
var currentBPM = Conductor.instance.bpm;
var currentBeat = Conductor.instance.currentBeat;
var currentStep = Conductor.instance.currentStep;
The Conductor:
  1. Loads time changes from song metadata
  2. Calculates beat/step times based on BPM
  3. Fires timing signals (stepHit, beatHit, measureHit)
  4. Provides timing utilities for gameplay

Example: Complete Song Setup

Directory structure:
assets/
  data/songs/myawesomesong/
    myawesomesong-metadata.json
    myawesomesong-chart.json
  songs/myawesomesong/
    Inst.ogg
    Voices-Player.ogg
    Voices-Opponent.ogg
myawesomesong-metadata.json:
{
  "version": "2.2.4",
  "songName": "My Awesome Song",
  "artist": "Cool Artist",
  "charter": "Me",
  "timeFormat": "ms",
  "timeChanges": [
    {
      "t": 0,
      "bpm": 140,
      "n": 4,
      "d": 4
    }
  ],
  "offsets": {
    "instrumental": 0,
    "vocals": {}
  },
  "playData": {
    "difficulties": ["easy", "normal", "hard"],
    "characters": {
      "player": "bf",
      "girlfriend": "gf",
      "opponent": "dad"
    },
    "stage": "mainStage",
    "noteStyle": "funkin",
    "ratings": {
      "easy": 2,
      "normal": 5,
      "hard": 8
    },
    "previewStart": 5000,
    "previewEnd": 20000
  }
}

Build docs developers (and LLMs) love