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:
Unique song identifier (e.g., “tutorial”, “bopeebo”)
Display name shown to players
Person who created the chart
Current variation (e.g., “default”, “erect”)
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 format version (currently “2.2.4”)
Person who charted the song
Number of divisions per beat (used for chart precision)
Whether the song should loop
Time format: "ms" (milliseconds), "ticks", or "float"
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
Timestamp when the change occurs (in the format specified by timeFormat)
New BPM value (quarter notes per minute)
Time signature numerator (the ‘4’ in 4/4)
Time signature denominator (the ‘4’ in 4/4)
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
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.
Available difficulty levels for this song
characters
SongCharacterData
required
Character IDs for player, opponent, and girlfriend
Stage ID to use for this song
Difficulty ratings shown in freeplay (1-10 scale)
Album ID for freeplay display
Start time for audio preview in freeplay (milliseconds)
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"]
}
}
Default instrumental track ID (empty string = default)
altInstrumentals
Array<String>
default:"[]"
List of alternate instrumental IDs
Character IDs whose vocals are in the player track
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):
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
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:
- Loads time changes from song metadata
- Calculates beat/step times based on BPM
- Fires timing signals (stepHit, beatHit, measureHit)
- 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
}
}