Merge pull request #114 from FunkinCrew/bugfix/conductor-monster

Fixes to Conductor for Monster
This commit is contained in:
Cameron Taylor 2023-07-22 12:59:51 -04:00 committed by GitHub
commit 9e40ed9006
13 changed files with 262 additions and 164 deletions

View file

@ -1,23 +1,18 @@
package funkin;
import funkin.play.song.SongData.SongTimeChange;
import funkin.util.Constants;
import flixel.util.FlxSignal;
import flixel.math.FlxMath;
import funkin.SongLoad.SwagSong;
import funkin.play.song.Song.SongDifficulty;
typedef BPMChangeEvent =
{
var stepTime:Int;
var songTime:Float;
var bpm:Float;
}
import funkin.play.song.SongData.SongTimeChange;
/**
* A global source of truth for timing information.
* A core class which handles musical timing throughout the game,
* both in gameplay and in menus.
*/
class Conductor
{
static final STEPS_PER_BEAT:Int = 4;
// onBeatHit is called every quarter note
// onStepHit is called every sixteenth note
// 4/4 = 4 beats per measure = 16 steps per measure
@ -33,11 +28,22 @@ class Conductor
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
// 7/8 = 3.5 beats per measure = 14 steps per measure
/**
* The list of time changes in the song.
* There should be at least one time change (at the beginning of the song) to define the BPM.
*/
static var timeChanges:Array<SongTimeChange> = [];
/**
* The current time change.
*/
static var currentTimeChange:SongTimeChange;
/**
* The current position in the song in milliseconds.
* Updated every frame based on the audio position.
*/
public static var songPosition:Float;
public static var songPosition:Float = 0;
/**
* Beats per minute of the current song at the current time.
@ -48,33 +54,17 @@ class Conductor
{
if (bpmOverride != null) return bpmOverride;
if (currentTimeChange == null) return 100;
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
return currentTimeChange.bpm;
}
/**
* The current value set by `forceBPM`.
* If false, BPM is determined by time changes.
*/
static var bpmOverride:Null<Float> = null;
/**
* Current position in the song, in whole measures.
*/
public static var currentMeasure(default, null):Int;
/**
* Current position in the song, in whole beats.
**/
public static var currentBeat(default, null):Int;
/**
* Current position in the song, in whole steps.
*/
public static var currentStep(default, null):Int;
/**
* Current position in the song, in steps and fractions of a step.
*/
public static var currentStepTime(default, null):Float;
/**
* Duration of a measure in milliseconds. Calculated based on bpm.
*/
@ -86,119 +76,99 @@ class Conductor
}
/**
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
* Duration of a beat in milliseconds. Calculated based on bpm.
*/
public static var beatLengthMs(get, null):Float;
static function get_beatLengthMs():Float
{
// Tied directly to BPM.
return ((60 / bpm) * 1000);
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
}
/**
* Duration of a step (sixteenth) in milliseconds. Calculated based on bpm.
* Duration of a step (quarter) in milliseconds. Calculated based on bpm.
*/
public static var stepLengthMs(get, null):Float;
static function get_stepLengthMs():Float
{
return beatLengthMs / STEPS_PER_BEAT;
return beatLengthMs / timeSignatureNumerator;
}
/**
* The numerator of the current time signature (number of notes in a measure)
*/
public static var timeSignatureNumerator(get, null):Int;
static function get_timeSignatureNumerator():Int
{
if (currentTimeChange == null) return 4;
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
return currentTimeChange.timeSignatureNum;
}
/**
* The numerator of the current time signature (length of notes in a measure)
*/
public static var timeSignatureDenominator(get, null):Int;
static function get_timeSignatureDenominator():Int
{
if (currentTimeChange == null) return 4;
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
return currentTimeChange.timeSignatureDen;
}
public static var offset:Float = 0;
// TODO: What's the difference between visualOffset and audioOffset?
public static var visualOffset:Float = 0;
public static var audioOffset:Float = 0;
//
// Signals
//
/**
* Current position in the song, in measures.
*/
public static var currentMeasure(default, null):Int;
/**
* Signal that is dispatched every measure.
* At 120 BPM 4/4, this is dispatched every 2 seconds.
* At 120 BPM 3/4, this is dispatched every 1.5 seconds.
* Current position in the song, in beats.
*/
public static var measureHit(default, null):FlxSignal = new FlxSignal();
public static var currentBeat(default, null):Int;
/**
* Signal that is dispatched every beat.
* At 120 BPM 4/4, this is dispatched every 0.5 seconds.
* At 120 BPM 3/4, this is dispatched every 0.5 seconds.
* Current position in the song, in steps.
*/
public static var currentStep(default, null):Int;
/**
* Current position in the song, in measures and fractions of a measure.
*/
public static var currentMeasureTime(default, null):Float;
/**
* Current position in the song, in beats and fractions of a measure.
*/
public static var currentBeatTime(default, null):Float;
/**
* Current position in the song, in steps and fractions of a step.
*/
public static var currentStepTime(default, null):Float;
public static var beatHit(default, null):FlxSignal = new FlxSignal();
/**
* Signal that is dispatched when a step is hit.
* At 120 BPM 4/4, this is dispatched every 0.125 seconds.
* At 120 BPM 3/4, this is dispatched every 0.125 seconds.
*/
public static var stepHit(default, null):FlxSignal = new FlxSignal();
//
// Internal Variables
//
/**
* The list of time changes in the song.
* There should be at least one time change (at the beginning of the song) to define the BPM.
*/
static var timeChanges:Array<SongTimeChange> = [];
/**
* The current time change.
*/
static var currentTimeChange:SongTimeChange;
public static var lastSongPos:Float;
public static var visualOffset:Float = 0;
public static var audioOffset:Float = 0;
public static var offset:Float = 0;
/**
* The number of beats (whole notes) in a measure.
*/
public static var beatsPerMeasure(get, null):Int;
public static var beatsPerMeasure(get, null):Float;
static function get_beatsPerMeasure():Int
static function get_beatsPerMeasure():Float
{
return timeSignatureNumerator;
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
}
/**
* The number of steps (quarter-notes) in a measure.
*/
public static var stepsPerMeasure(get, null):Int;
static function get_stepsPerMeasure():Int
{
// This is always 4, b
return timeSignatureNumerator * 4;
// TODO: Is this always an integer?
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
}
function new() {}
/**
* Forcibly defines the current BPM of the song.
* Useful for things like the chart editor that need to manipulate BPM in real time.
@ -208,16 +178,11 @@ class Conductor
* WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead.
*/
public static function forceBPM(?bpm:Float = null):Void
public static function forceBPM(?bpm:Float = null)
{
if (bpm != null)
{
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
}
if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm);
else
{
trace('[CONDUCTOR] Resetting BPM to default');
}
Conductor.bpmOverride = bpm;
}
@ -228,13 +193,12 @@ class Conductor
* @param songPosition The current position in the song in milliseconds.
* Leave blank to use the FlxG.sound.music position.
*/
public static function update(songPosition:Float = null):Void
public static function update(songPosition:Float = null)
{
if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
var oldMeasure:Int = currentMeasure;
var oldBeat:Int = currentBeat;
var oldStep:Int = currentStep;
var oldBeat = currentBeat;
var oldStep = currentStep;
Conductor.songPosition = songPosition;
@ -252,16 +216,23 @@ class Conductor
}
else if (currentTimeChange != null)
{
currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs;
// roundDecimal prevents representing 8 as 7.9999999
currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
currentMeasureTime = currentStepTime / stepsPerMeasure;
currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentStep / 4);
currentBeat = Math.floor(currentBeatTime);
currentMeasure = Math.floor(currentMeasureTime);
}
else
{
// Assume a constant BPM equal to the forced value.
currentStepTime = (songPosition / stepLengthMs);
currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
currentMeasureTime = currentStepTime / stepsPerMeasure;
currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentStep / 4);
currentBeat = Math.floor(currentBeatTime);
currentMeasure = Math.floor(currentMeasureTime);
}
// FlxSignals are really cool.
@ -274,31 +245,52 @@ class Conductor
{
beatHit.dispatch();
}
if (currentMeasure != oldMeasure)
{
measureHit.dispatch();
}
}
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
{
timeChanges = [];
for (currentTimeChange in songTimeChanges)
{
// TODO: Maybe handle this different?
// Do we care about BPM at negative timestamps?
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
if (currentTimeChange.beatTime == null)
{
if (currentTimeChange.timeStamp <= 0.0)
{
currentTimeChange.beatTime = 0.0;
}
else
{
// Calculate the beat time of this timestamp.
currentTimeChange.beatTime = 0.0;
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
{
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
currentTimeChange.beatTime = prevTimeChange.beatTime
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC);
}
}
}
timeChanges.push(currentTimeChange);
}
trace('Done mapping time changes: ' + timeChanges);
// Done.
// Update currentStepTime
Conductor.update(Conductor.songPosition);
}
/**
* Given a time in milliseconds, return a time in steps.
*/
public static function getTimeInSteps(ms:Float):Int
public static function getTimeInSteps(ms:Float):Float
{
if (timeChanges.length == 0)
{
@ -307,7 +299,7 @@ class Conductor
}
else
{
var resultStep:Int = 0;
var resultStep:Float = 0;
var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges)
@ -329,4 +321,14 @@ class Conductor
return resultStep;
}
}
public static function reset():Void
{
beatHit.removeAll();
stepHit.removeAll();
mapTimeChanges([]);
forceBPM(null);
update(0);
}
}

View file

@ -67,9 +67,11 @@ class MusicBeatState extends FlxUIState implements IEventHandler
if (FlxG.keys.justPressed.F5) debug_refreshModules();
// Display Conductor info in the watch window.
FlxG.watch.addQuick("songPos", Conductor.songPosition);
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
FlxG.watch.addQuick("bpm", Conductor.bpm);
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
dispatchEvent(new UpdateScriptEvent(elapsed));
}

View file

@ -3,7 +3,6 @@ package funkin;
import flixel.FlxSubState;
import funkin.modding.IScriptedClass.IEventHandler;
import flixel.util.FlxColor;
import funkin.Conductor.BPMChangeEvent;
import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler;
import flixel.text.FlxText;

View file

@ -6,7 +6,8 @@ import openfl.utils.Assets as OpenFlAssets;
class Paths
{
inline public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
public static var VIDEO_EXT = "mp4";
static var currentLevel:String;
@ -96,14 +97,19 @@ class Paths
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
}
inline static public function voices(song:String, ?suffix:String)
inline static public function videos(key:String, ?library:String)
{
return getPath('videos/$key.$VIDEO_EXT', BINARY, library);
}
inline static public function voices(song:String, ?suffix:String = '')
{
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
}
inline static public function inst(song:String, ?suffix:String)
inline static public function inst(song:String, ?suffix:String = '')
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
}

View file

@ -4,7 +4,7 @@ import funkin.play.PlayStatePlaylist;
import flixel.FlxSprite;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.system.FlxSound;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;

View file

@ -12,6 +12,8 @@ import flixel.util.FlxTimer;
import funkin.audiovis.SpectogramSprite;
import funkin.shaderslmfao.ColorSwap;
import funkin.shaderslmfao.LeftMaskShader;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongMetadata;
import funkin.shaderslmfao.TitleOutline;
import funkin.ui.AtlasText;
import funkin.util.Constants;
@ -135,12 +137,7 @@ class TitleState extends MusicBeatState
function startIntro()
{
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
{
FlxG.sound.playMusic(Paths.music('freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
}
playMenuMusic();
persistentUpdate = true;
@ -234,6 +231,18 @@ class TitleState extends MusicBeatState
if (FlxG.sound.music != null) FlxG.sound.music.onComplete = function() FlxG.switchState(new VideoState());
}
function playMenuMusic():Void
{
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
{
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
}
function getIntroTextShit():Array<Array<String>>
{
var fullText:String = Assets.getText(Paths.txt('introText'));

View file

@ -3,7 +3,7 @@ package funkin.play;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.system.FlxSound;
import flixel.sound.FlxSound;
import funkin.ui.story.StoryMenuState;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;

View file

@ -894,7 +894,7 @@ class PlayState extends MusicBeatState
trace('Song difficulty could not be loaded.');
}
Conductor.forceBPM(currentChart.getStartingBPM());
// Conductor.forceBPM(currentChart.getStartingBPM());
vocals = currentChart.buildVocals(currentPlayerId);
if (vocals.members.length == 0)
@ -1208,13 +1208,10 @@ class PlayState extends MusicBeatState
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
}
FlxG.watch.addQuick('beatShit', Conductor.currentBeat);
FlxG.watch.addQuick('stepShit', Conductor.currentStep);
if (currentStage != null)
{
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
}
FlxG.watch.addQuick('songPos', Conductor.songPosition);
// Handle GF dance speed.
// TODO: Add a song event for this.

View file

@ -30,7 +30,8 @@ class VideoCutscene
if (!openfl.Assets.exists(filePath))
{
trace('ERROR: Video file does not exist: ${filePath}');
// Display a popup.
lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
return;
}

View file

@ -3,6 +3,8 @@ package funkin.play.song;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import flixel.util.typeLimit.OneOfTwo;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.song.ScriptedSong;
import funkin.util.assets.DataAssets;
import haxe.DynamicAccess;
@ -22,6 +24,7 @@ class SongDataParser
static final DEFAULT_SONG_ID:String = 'UNKNOWN';
static final SONG_DATA_PATH:String = 'songs/';
static final MUSIC_DATA_PATH:String = 'music/';
static final SONG_DATA_SUFFIX:String = '-metadata.json';
/**
@ -181,6 +184,36 @@ class SongDataParser
return rawJson;
}
public static function parseMusicMetadata(musicId:String):SongMetadata
{
var rawJson:String = loadMusicMetadataFile(musicId);
var jsonData:Dynamic = null;
try
{
jsonData = Json.parse(rawJson);
}
catch (e) {}
var musicMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, musicId);
musicMetadata = SongValidator.validateSongMetadata(musicMetadata, musicId);
return musicMetadata;
}
static function loadMusicMetadataFile(musicPath:String, variation:String = ''):String
{
var musicMetadataFilePath:String = (variation != '') ? Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata-$variation.json') : Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata.json');
var rawJson:String = Assets.getText(musicMetadataFilePath).trim();
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return rawJson;
}
public static function parseSongChartData(songId:String, variation:String = ""):SongChartData
{
var rawJson:String = loadSongChartDataFile(songId, variation);
@ -369,8 +402,7 @@ abstract SongNoteData(RawSongNoteData)
public function get_stepTime():Float
{
// TODO: Account for changes in BPM.
return this.t / Conductor.stepLengthMs;
return Conductor.getTimeInSteps(this.t);
}
/**
@ -555,8 +587,7 @@ abstract SongEventData(RawSongEventData)
public function get_stepTime():Float
{
// TODO: Account for changes in BPM.
return this.t / Conductor.stepLengthMs;
return Conductor.getTimeInSteps(this.t);
}
public var event(get, set):String;
@ -769,7 +800,7 @@ typedef RawSongTimeChange =
* Time in beats (int). The game will calculate further beat values based on this one,
* so it can do it in a simple linear fashion.
*/
var b:Int;
var b:Null<Float>;
/**
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
@ -799,9 +830,9 @@ typedef RawSongTimeChange =
* Add aliases to the minimalized property names of the typedef,
* to improve readability.
*/
abstract SongTimeChange(RawSongTimeChange)
abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
{
public function new(timeStamp:Float, beatTime:Int, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
public function new(timeStamp:Float, beatTime:Null<Float>, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
{
this =
{
@ -826,14 +857,14 @@ abstract SongTimeChange(RawSongTimeChange)
return this.t = value;
}
public var beatTime(get, set):Int;
public var beatTime(get, set):Null<Float>;
public function get_beatTime():Int
public function get_beatTime():Null<Float>
{
return this.b;
}
public function set_beatTime(value:Int):Int
public function set_beatTime(value:Null<Float>):Null<Float>
{
return this.b = value;
}

View file

@ -1877,7 +1877,7 @@ class ChartEditorState extends HaxeUIState
{
// Handle extending the note as you drag.
// Since use Math.floor and stepCrochet here, the hold notes will be beat snapped.
// Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped.
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs);
// Without this, the newly placed note feels too short compared to the user's input.

View file

@ -15,6 +15,7 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongDataParser;
import funkin.util.Constants;
@ -115,12 +116,7 @@ class StoryMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut;
if (!FlxG.sound.music.playing)
{
FlxG.sound.playMusic(Paths.music('freakyMenu'));
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
playMenuMusic();
if (stickerSubState != null)
{
@ -129,8 +125,6 @@ class StoryMenuState extends MusicBeatState
openSubState(stickerSubState);
stickerSubState.degenStickers();
// resetSubState();
}
persistentUpdate = persistentDraw = true;
@ -203,6 +197,18 @@ class StoryMenuState extends MusicBeatState
#end
}
function playMenuMusic():Void
{
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
{
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
}
function updateData():Void
{
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);

View file

@ -118,27 +118,72 @@ class Constants
public static final DEFAULT_SONG:String = 'tutorial';
/**
* OTHER
* TIMING
*/
// ==============================
/**
* The number of seconds in a minute.
*/
public static final SECS_PER_MIN:Int = 60;
/**
* The number of milliseconds in a second.
*/
public static final MS_PER_SEC:Int = 1000;
/**
* The number of microseconds in a millisecond.
*/
public static final US_PER_MS:Int = 1000;
/**
* The number of microseconds in a second.
*/
public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
/**
* The number of nanoseconds in a microsecond.
*/
public static final NS_PER_US:Int = 1000;
/**
* The number of nanoseconds in a millisecond.
*/
public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
/**
* The number of nanoseconds in a second.
*/
public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
/**
* All MP3 decoders introduce a playback delay of `528` samples,
* which at 44,100 Hz (samples per second) is ~12 ms.
*/
public static final MP3_DELAY_MS:Float = 528 / 44100 * 1000;
public static final MP3_DELAY_MS:Float = 528 / 44100 * MS_PER_SEC;
/**
* The default BPM of the conductor.
*/
public static final DEFAULT_BPM:Float = 100.0;
public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
public static final STEPS_PER_BEAT:Int = 4;
/**
* OTHER
*/
// ==============================
/**
* The scale factor to use when increasing the size of pixel art graphics.
*/
public static final PIXEL_ART_SCALE:Float = 6;
/**
* The BPM of the title screen and menu music.
* TODO: Move to metadata file.
*/
public static final FREAKY_MENU_BPM:Float = 102;
/**
* The volume at which to play the countdown before the song starts.
*/