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; package funkin;
import funkin.play.song.SongData.SongTimeChange; import funkin.util.Constants;
import flixel.util.FlxSignal; import flixel.util.FlxSignal;
import flixel.math.FlxMath;
import funkin.SongLoad.SwagSong;
import funkin.play.song.Song.SongDifficulty; import funkin.play.song.Song.SongDifficulty;
import funkin.play.song.SongData.SongTimeChange;
typedef BPMChangeEvent =
{
var stepTime:Int;
var songTime:Float;
var bpm:Float;
}
/** /**
* 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 class Conductor
{ {
static final STEPS_PER_BEAT:Int = 4;
// onBeatHit is called every quarter note // onBeatHit is called every quarter note
// onStepHit is called every sixteenth note // onStepHit is called every sixteenth note
// 4/4 = 4 beats per measure = 16 steps per measure // 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 // 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
// 7/8 = 3.5 beats per measure = 14 steps per measure // 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. * The current position in the song in milliseconds.
* Updated every frame based on the audio position. * 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. * Beats per minute of the current song at the current time.
@ -48,33 +54,17 @@ class Conductor
{ {
if (bpmOverride != null) return bpmOverride; if (bpmOverride != null) return bpmOverride;
if (currentTimeChange == null) return 100; if (currentTimeChange == null) return Constants.DEFAULT_BPM;
return currentTimeChange.bpm; return currentTimeChange.bpm;
} }
/**
* The current value set by `forceBPM`.
* If false, BPM is determined by time changes.
*/
static var bpmOverride:Null<Float> = null; 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. * 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; public static var beatLengthMs(get, null):Float;
static function get_beatLengthMs():Float static function get_beatLengthMs():Float
{ {
// Tied directly to BPM. return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
return ((60 / bpm) * 1000);
} }
/** /**
* 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; public static var stepLengthMs(get, null):Float;
static function get_stepLengthMs():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; public static var timeSignatureNumerator(get, null):Int;
static function get_timeSignatureNumerator():Int static function get_timeSignatureNumerator():Int
{ {
if (currentTimeChange == null) return 4; if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
return currentTimeChange.timeSignatureNum; return currentTimeChange.timeSignatureNum;
} }
/**
* The numerator of the current time signature (length of notes in a measure)
*/
public static var timeSignatureDenominator(get, null):Int; public static var timeSignatureDenominator(get, null):Int;
static function get_timeSignatureDenominator():Int static function get_timeSignatureDenominator():Int
{ {
if (currentTimeChange == null) return 4; if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
return currentTimeChange.timeSignatureDen; return currentTimeChange.timeSignatureDen;
} }
public static var offset:Float = 0; /**
* Current position in the song, in measures.
// TODO: What's the difference between visualOffset and audioOffset? */
public static var visualOffset:Float = 0; public static var currentMeasure(default, null):Int;
public static var audioOffset:Float = 0;
//
// Signals
//
/** /**
* Signal that is dispatched every measure. * Current position in the song, in beats.
* At 120 BPM 4/4, this is dispatched every 2 seconds.
* At 120 BPM 3/4, this is dispatched every 1.5 seconds.
*/ */
public static var measureHit(default, null):FlxSignal = new FlxSignal(); public static var currentBeat(default, null):Int;
/** /**
* Signal that is dispatched every beat. * Current position in the song, in steps.
* At 120 BPM 4/4, this is dispatched every 0.5 seconds.
* At 120 BPM 3/4, this is dispatched every 0.5 seconds.
*/ */
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(); 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(); 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 lastSongPos:Float;
public static var visualOffset:Float = 0;
public static var audioOffset:Float = 0;
public static var offset:Float = 0;
/** public static var beatsPerMeasure(get, null):Float;
* The number of beats (whole notes) in a measure.
*/
public static var beatsPerMeasure(get, null):Int;
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; public static var stepsPerMeasure(get, null):Int;
static function get_stepsPerMeasure():Int static function get_stepsPerMeasure():Int
{ {
// This is always 4, b // TODO: Is this always an integer?
return timeSignatureNumerator * 4; return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
} }
function new() {}
/** /**
* Forcibly defines the current BPM of the song. * Forcibly defines the current BPM of the song.
* Useful for things like the chart editor that need to manipulate BPM in real time. * 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, * WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead. * 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) if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm);
{
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
}
else else
{
trace('[CONDUCTOR] Resetting BPM to default'); trace('[CONDUCTOR] Resetting BPM to default');
}
Conductor.bpmOverride = bpm; Conductor.bpmOverride = bpm;
} }
@ -228,13 +193,12 @@ class Conductor
* @param songPosition The current position in the song in milliseconds. * @param songPosition The current position in the song in milliseconds.
* Leave blank to use the FlxG.sound.music position. * 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; if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
var oldMeasure:Int = currentMeasure; var oldBeat = currentBeat;
var oldBeat:Int = currentBeat; var oldStep = currentStep;
var oldStep:Int = currentStep;
Conductor.songPosition = songPosition; Conductor.songPosition = songPosition;
@ -252,16 +216,23 @@ class Conductor
} }
else if (currentTimeChange != null) 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); currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentStep / 4); currentBeat = Math.floor(currentBeatTime);
currentMeasure = Math.floor(currentMeasureTime);
} }
else else
{ {
// Assume a constant BPM equal to the forced value. // 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); currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentStep / 4); currentBeat = Math.floor(currentBeatTime);
currentMeasure = Math.floor(currentMeasureTime);
} }
// FlxSignals are really cool. // FlxSignals are really cool.
@ -274,31 +245,52 @@ class Conductor
{ {
beatHit.dispatch(); beatHit.dispatch();
} }
if (currentMeasure != oldMeasure)
{
measureHit.dispatch();
}
} }
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
{ {
timeChanges = []; timeChanges = [];
for (currentTimeChange in songTimeChanges) 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); timeChanges.push(currentTimeChange);
} }
trace('Done mapping time changes: ' + timeChanges); trace('Done mapping time changes: ' + timeChanges);
// Done. // Update currentStepTime
Conductor.update(Conductor.songPosition);
} }
/** /**
* Given a time in milliseconds, return a time in steps. * 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) if (timeChanges.length == 0)
{ {
@ -307,7 +299,7 @@ class Conductor
} }
else else
{ {
var resultStep:Int = 0; var resultStep:Float = 0;
var lastTimeChange:SongTimeChange = timeChanges[0]; var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges) for (timeChange in timeChanges)
@ -329,4 +321,14 @@ class Conductor
return resultStep; 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(); if (FlxG.keys.justPressed.F5) debug_refreshModules();
// Display Conductor info in the watch window. // Display Conductor info in the watch window.
FlxG.watch.addQuick("songPos", Conductor.songPosition); FlxG.watch.addQuick("songPosition", Conductor.songPosition);
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
FlxG.watch.addQuick("bpm", Conductor.bpm); 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)); dispatchEvent(new UpdateScriptEvent(elapsed));
} }

View file

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

View file

@ -6,7 +6,8 @@ import openfl.utils.Assets as OpenFlAssets;
class Paths 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; static var currentLevel:String;
@ -96,14 +97,19 @@ class Paths
return getPath('music/$key.$SOUND_EXT', MUSIC, library); 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 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'; 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'; 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.FlxSprite;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.system.FlxSound; import flixel.sound.FlxSound;
import flixel.text.FlxText; import flixel.text.FlxText;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;

View file

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

View file

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

View file

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

View file

@ -30,7 +30,8 @@ class VideoCutscene
if (!openfl.Assets.exists(filePath)) 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; return;
} }

View file

@ -3,6 +3,8 @@ package funkin.play.song;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import flixel.util.typeLimit.OneOfTwo; import flixel.util.typeLimit.OneOfTwo;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.song.ScriptedSong; import funkin.play.song.ScriptedSong;
import funkin.util.assets.DataAssets; import funkin.util.assets.DataAssets;
import haxe.DynamicAccess; import haxe.DynamicAccess;
@ -22,6 +24,7 @@ class SongDataParser
static final DEFAULT_SONG_ID:String = 'UNKNOWN'; static final DEFAULT_SONG_ID:String = 'UNKNOWN';
static final SONG_DATA_PATH:String = 'songs/'; static final SONG_DATA_PATH:String = 'songs/';
static final MUSIC_DATA_PATH:String = 'music/';
static final SONG_DATA_SUFFIX:String = '-metadata.json'; static final SONG_DATA_SUFFIX:String = '-metadata.json';
/** /**
@ -181,6 +184,36 @@ class SongDataParser
return rawJson; 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 public static function parseSongChartData(songId:String, variation:String = ""):SongChartData
{ {
var rawJson:String = loadSongChartDataFile(songId, variation); var rawJson:String = loadSongChartDataFile(songId, variation);
@ -369,8 +402,7 @@ abstract SongNoteData(RawSongNoteData)
public function get_stepTime():Float public function get_stepTime():Float
{ {
// TODO: Account for changes in BPM. return Conductor.getTimeInSteps(this.t);
return this.t / Conductor.stepLengthMs;
} }
/** /**
@ -555,8 +587,7 @@ abstract SongEventData(RawSongEventData)
public function get_stepTime():Float public function get_stepTime():Float
{ {
// TODO: Account for changes in BPM. return Conductor.getTimeInSteps(this.t);
return this.t / Conductor.stepLengthMs;
} }
public var event(get, set):String; 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, * 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. * 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, * 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, * Add aliases to the minimalized property names of the typedef,
* to improve readability. * 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 = this =
{ {
@ -826,14 +857,14 @@ abstract SongTimeChange(RawSongTimeChange)
return this.t = value; 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; return this.b;
} }
public function set_beatTime(value:Int):Int public function set_beatTime(value:Null<Float>):Null<Float>
{ {
return this.b = value; return this.b = value;
} }

View file

@ -1877,7 +1877,7 @@ class ChartEditorState extends HaxeUIState
{ {
// Handle extending the note as you drag. // 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); 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. // 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.PlayState;
import funkin.play.PlayStatePlaylist; import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongDataParser;
import funkin.util.Constants; import funkin.util.Constants;
@ -115,12 +116,7 @@ class StoryMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn; transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut; transOut = FlxTransitionableState.defaultTransOut;
if (!FlxG.sound.music.playing) playMenuMusic();
{
FlxG.sound.playMusic(Paths.music('freakyMenu'));
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
if (stickerSubState != null) if (stickerSubState != null)
{ {
@ -129,8 +125,6 @@ class StoryMenuState extends MusicBeatState
openSubState(stickerSubState); openSubState(stickerSubState);
stickerSubState.degenStickers(); stickerSubState.degenStickers();
// resetSubState();
} }
persistentUpdate = persistentDraw = true; persistentUpdate = persistentDraw = true;
@ -203,6 +197,18 @@ class StoryMenuState extends MusicBeatState
#end #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 function updateData():Void
{ {
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId); currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);

View file

@ -118,27 +118,72 @@ class Constants
public static final DEFAULT_SONG:String = 'tutorial'; 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, * All MP3 decoders introduce a playback delay of `528` samples,
* which at 44,100 Hz (samples per second) is ~12 ms. * 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. * The scale factor to use when increasing the size of pixel art graphics.
*/ */
public static final PIXEL_ART_SCALE:Float = 6; 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. * The volume at which to play the countdown before the song starts.
*/ */