From 4bf9f686586e03b197312afdb824b48dd3455bbb Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 9 Feb 2024 14:58:57 -0500 Subject: [PATCH 1/7] Finalize freeplay preview toolbox --- assets | 2 +- source/funkin/audio/FunkinSound.hx | 24 + source/funkin/audio/SoundGroup.hx | 39 + source/funkin/audio/VoicesGroup.hx | 8 +- source/funkin/audio/waveform/WaveformData.hx | 32 + .../audio/waveform/WaveformDataParser.hx | 29 +- source/funkin/data/song/SongData.hx | 23 +- source/funkin/data/song/SongRegistry.hx | 2 +- .../ui/debug/charting/ChartEditorState.hx | 119 +-- .../commands/SetAudioOffsetCommand.hx | 6 +- .../commands/SetFreeplayPreviewCommand.hx | 62 ++ .../handlers/ChartEditorAudioHandler.hx | 43 +- .../ChartEditorImportExportHandler.hx | 5 + .../handlers/ChartEditorToolboxHandler.hx | 14 + .../toolboxes/ChartEditorFreeplayToolbox.hx | 693 ++++++++++++++++++ .../toolboxes/ChartEditorOffsetsToolbox.hx | 15 +- source/funkin/ui/freeplay/FreeplayState.hx | 25 + 17 files changed, 1067 insertions(+), 74 deletions(-) create mode 100644 source/funkin/ui/debug/charting/commands/SetFreeplayPreviewCommand.hx create mode 100644 source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx diff --git a/assets b/assets index 251d4640b..1941ec605 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 251d4640bd77ee0f0b6122a13f123274c43dd3f5 +Subproject commit 1941ec605d2da5a27e41588515d13d5bfc882582 diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index bc35cc0a7..e7ce68d08 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -8,6 +8,8 @@ import flixel.sound.FlxSound; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.system.FlxAssets.FlxSoundAsset; import funkin.util.tools.ICloneable; +import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformDataParser; import flixel.math.FlxMath; import openfl.Assets; #if (openfl >= "8.0.0") @@ -58,6 +60,24 @@ class FunkinSound extends FlxSound implements ICloneable return this.playing || this._shouldPlay; } + /** + * Waveform data for this sound. + * This is lazily loaded, so it will be built the first time it is accessed. + */ + public var waveformData(get, never):WaveformData; + + var _waveformData:Null = null; + + function get_waveformData():WaveformData + { + if (_waveformData == null) + { + _waveformData = WaveformDataParser.interpretFlxSound(this); + if (_waveformData == null) throw 'Could not interpret waveform data!'; + } + return _waveformData; + } + /** * Are we in a state where the song should play but time is negative? */ @@ -218,6 +238,10 @@ class FunkinSound extends FlxSound implements ICloneable // Call init to ensure the FlxSound is properly initialized. sound.init(this.looped, this.autoDestroy, this.onComplete); + // Oh yeah, the waveform data is the same too! + @:privateAccess + sound._waveformData = this._waveformData; + return sound; } diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 64104fee7..df3a67ae1 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -3,6 +3,7 @@ package funkin.audio; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.sound.FlxSound; import funkin.audio.FunkinSound; +import flixel.tweens.FlxTween; /** * A group of FunkinSounds that are all synced together. @@ -14,6 +15,8 @@ class SoundGroup extends FlxTypedGroup public var volume(get, set):Float; + public var muted(get, set):Bool; + public var pitch(get, set):Float; public var playing(get, never):Bool; @@ -124,6 +127,26 @@ class SoundGroup extends FlxTypedGroup }); } + /** + * Fade in all the sounds in the group. + */ + public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void + { + forEachAlive(function(sound:FunkinSound) { + sound.fadeIn(duration, from, to, onComplete); + }); + } + + /** + * Fade out all the sounds in the group. + */ + public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void + { + forEachAlive(function(sound:FunkinSound) { + sound.fadeOut(duration, to, onComplete); + }); + } + /** * Stop all the sounds in the group. */ @@ -191,6 +214,22 @@ class SoundGroup extends FlxTypedGroup return volume; } + function get_muted():Bool + { + if (getFirstAlive() != null) return getFirstAlive().muted; + else + return false; + } + + function set_muted(muted:Bool):Bool + { + forEachAlive(function(snd:FunkinSound) { + snd.muted = muted; + }); + + return muted; + } + function get_pitch():Float { #if FLX_PITCH diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 70a01f9dc..5daebc89d 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -116,18 +116,18 @@ class VoicesGroup extends SoundGroup return opponentVoices.members[index]; } - public function buildPlayerVoiceWaveform():Null + public function getPlayerVoiceWaveform():Null { if (playerVoices.members.length == 0) return null; - return WaveformDataParser.interpretFlxSound(playerVoices.members[0]); + return playerVoices.members[0].waveformData; } - public function buildOpponentVoiceWaveform():Null + public function getOpponentVoiceWaveform():Null { if (opponentVoices.members.length == 0) return null; - return WaveformDataParser.interpretFlxSound(opponentVoices.members[0]); + return opponentVoices.members[0].waveformData; } /** diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx index 31f8dfe02..b82d141e7 100644 --- a/source/funkin/audio/waveform/WaveformData.hx +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -182,6 +182,38 @@ class WaveformData return result; } + /** + * Create a new WaveformData whose data represents the two waveforms overlayed. + */ + public function merge(that:WaveformData):WaveformData + { + var result = this.clone([]); + + for (channelIndex in 0...this.channels) + { + var thisChannel = this.channel(channelIndex); + var thatChannel = that.channel(channelIndex); + var resultChannel = result.channel(channelIndex); + + for (index in 0...this.length) + { + var thisMinSample = thisChannel.minSample(index); + var thatMinSample = thatChannel.minSample(index); + + var thisMaxSample = thisChannel.maxSample(index); + var thatMaxSample = thatChannel.maxSample(index); + + resultChannel.setMinSample(index, Std.int(Math.min(thisMinSample, thatMinSample))); + resultChannel.setMaxSample(index, Std.int(Math.max(thisMaxSample, thatMaxSample))); + } + } + + @:privateAccess + result.length = this.length; + + return result; + } + /** * Create a new WaveformData whose parameters match the current object. */ diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx index 2e5c52d13..54a142f6a 100644 --- a/source/funkin/audio/waveform/WaveformDataParser.hx +++ b/source/funkin/audio/waveform/WaveformDataParser.hx @@ -29,12 +29,12 @@ class WaveformDataParser } else { - trace('[WAVEFORM] Method 2 worked.'); + // trace('[WAVEFORM] Method 2 worked.'); } } else { - trace('[WAVEFORM] Method 1 worked.'); + // trace('[WAVEFORM] Method 1 worked.'); } return interpretAudioBuffer(soundBuffer); @@ -55,22 +55,24 @@ class WaveformDataParser var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1))); var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint)); - trace('Interpreting audio buffer:'); - trace(' sampleRate: ${sampleRate}'); - trace(' channels: ${channels}'); - trace(' bitsPerSample: ${bitsPerSample}'); - trace(' samplesPerPoint: ${samplesPerPoint}'); - trace(' pointsPerSecond: ${pointsPerSecond}'); - trace(' soundDataRawLength: ${soundDataRawLength}'); - trace(' soundDataSampleCount: ${soundDataSampleCount}'); - trace(' soundDataRawLength/4: ${soundDataRawLength / 4}'); - trace(' outputPointCount: ${outputPointCount}'); + // trace('Interpreting audio buffer:'); + // trace(' sampleRate: ${sampleRate}'); + // trace(' channels: ${channels}'); + // trace(' bitsPerSample: ${bitsPerSample}'); + // trace(' samplesPerPoint: ${samplesPerPoint}'); + // trace(' pointsPerSecond: ${pointsPerSecond}'); + // trace(' soundDataRawLength: ${soundDataRawLength}'); + // trace(' soundDataSampleCount: ${soundDataSampleCount}'); + // trace(' soundDataRawLength/4: ${soundDataRawLength / 4}'); + // trace(' outputPointCount: ${outputPointCount}'); var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN; var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX; var outputData:Array = []; + var perfStart = haxe.Timer.stamp(); + for (pointIndex in 0...outputPointCount) { // minChannel1, maxChannel1, minChannel2, maxChannel2, ... @@ -106,6 +108,9 @@ class WaveformDataParser var outputDataLength:Int = Std.int(outputData.length / channels / 2); var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData); + var perfEnd = haxe.Timer.stamp(); + trace('[WAVEFORM] Interpreted audio buffer in ${perfEnd - perfStart} seconds.'); + return result; } diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 52b9c19d6..7d5bc4e19 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -431,6 +431,24 @@ class SongPlayData implements ICloneable @:optional public var album:Null; + /** + * The start time for the audio preview in Freeplay. + * Defaults to 0 seconds in. + * @since `2.2.2` + */ + @:optional + @:default(0) + public var previewStart:Int; + + /** + * The end time for the audio preview in Freeplay. + * Defaults to 15 seconds in. + * @since `2.2.2` + */ + @:optional + @:default(15000) + public var previewEnd:Int; + public function new() { ratings = new Map(); @@ -438,6 +456,7 @@ class SongPlayData implements ICloneable public function clone():SongPlayData { + // TODO: This sucks! If you forget to update this you get weird behavior. var result:SongPlayData = new SongPlayData(); result.songVariations = this.songVariations.clone(); result.difficulties = this.difficulties.clone(); @@ -446,6 +465,8 @@ class SongPlayData implements ICloneable result.noteStyle = this.noteStyle; result.ratings = this.ratings.clone(); result.album = this.album; + result.previewStart = this.previewStart; + result.previewEnd = this.previewEnd; return result; } @@ -777,7 +798,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR var title = eventSchema.getByName(key)?.title ?? 'UnknownField'; - if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value)); + // if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value)); var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue'; result += '\n- ${title}: ${valueStr}'; diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index b772349bc..d2a548c62 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.1"; + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2"; public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index d0326be30..53325acb8 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,47 +1,52 @@ package funkin.ui.debug.charting; -import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; -import funkin.util.logging.CrashHandler; -import haxe.ui.containers.HBox; -import haxe.ui.containers.Grid; -import haxe.ui.containers.ScrollView; -import haxe.ui.containers.menus.MenuBar; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; +import flixel.graphics.FlxGraphic; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; -import funkin.graphics.FunkinSprite; import flixel.input.keyboard.FlxKey; +import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxMath; import flixel.math.FlxPoint; -import flixel.graphics.FlxGraphic; import flixel.math.FlxRect; import flixel.sound.FlxSound; +import flixel.system.debug.log.LogStyle; import flixel.system.FlxAssets.FlxSoundAsset; +import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.tweens.misc.VarTween; -import funkin.audio.waveform.WaveformSprite; -import haxe.ui.Toolkit; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; +import funkin.audio.FunkinSound; +import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.VoicesGroup; -import funkin.audio.FunkinSound; +import funkin.audio.waveform.WaveformSprite; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongData.SongCharacterData; +import funkin.data.song.SongData.SongCharacterData; +import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongMetadata; -import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongDataUtils; +import funkin.data.song.SongDataUtils; import funkin.data.song.SongRegistry; +import funkin.data.song.SongRegistry; +import funkin.data.stage.StageData; +import funkin.graphics.FunkinSprite; import funkin.input.Cursor; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; @@ -52,22 +57,14 @@ import funkin.play.components.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.PlayState; import funkin.play.song.Song; -import funkin.data.song.SongData.SongChartData; -import funkin.data.song.SongRegistry; -import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongData.SongCharacterData; -import funkin.data.song.SongDataUtils; -import funkin.ui.debug.charting.commands.ChartEditorCommand; -import funkin.data.stage.StageData; import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand; -import funkin.ui.debug.charting.commands.CutItemsCommand; +import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.CopyItemsCommand; +import funkin.ui.debug.charting.commands.CutItemsCommand; import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; import funkin.ui.debug.charting.commands.DeselectItemsCommand; import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; @@ -85,17 +82,22 @@ import funkin.ui.debug.charting.commands.SelectItemsCommand; import funkin.ui.debug.charting.commands.SetItemSelectionCommand; import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; +import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNoteSprite; -import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite; import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; +import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; +import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; +import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox; +import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.ui.mainmenu.MainMenuState; import funkin.util.Constants; import funkin.util.FileUtil; +import funkin.util.logging.CrashHandler; import funkin.util.SortUtil; import funkin.util.WindowUtil; import haxe.DynamicAccess; @@ -103,23 +105,26 @@ import haxe.io.Bytes; import haxe.io.Path; import haxe.ui.backend.flixel.UIRuntimeState; import haxe.ui.backend.flixel.UIState; -import haxe.ui.components.DropDown; -import haxe.ui.components.Label; import haxe.ui.components.Button; +import haxe.ui.components.DropDown; +import haxe.ui.components.Image; +import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; +import haxe.ui.containers.Box; import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.Frame; -import haxe.ui.containers.Box; +import haxe.ui.containers.Grid; +import haxe.ui.containers.HBox; import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.MenuBar; -import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuCheckBox; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.ScrollView; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; -import haxe.ui.components.Image; -import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; import haxe.ui.core.Component; import haxe.ui.core.Screen; import haxe.ui.events.DragEvent; @@ -127,12 +132,8 @@ import haxe.ui.events.MouseEvent; import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent; import haxe.ui.focus.FocusManager; +import haxe.ui.Toolkit; import openfl.display.BitmapData; -import funkin.audio.visualize.PolygonSpectogram; -import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.input.mouse.FlxMouseEvent; -import flixel.text.FlxText; -import flixel.system.debug.log.LogStyle; using Lambda; @@ -154,18 +155,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ // ============================== // Layouts - public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); - - public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); - public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties'); - public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); - public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets'); public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); + public static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview'); public static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview'); + public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); + public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets'); + public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); + public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay'); + public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties'); // Validation - public static final SUPPORTED_MUSIC_FORMATS:Array = ['ogg']; + public static final SUPPORTED_MUSIC_FORMATS:Array = #if sys ['ogg'] #else ['mp3'] #end; // Layout @@ -1311,6 +1313,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return currentSongMetadata.playData.noteStyle = value; } + var currentSongFreeplayPreviewStart(get, set):Int; + + function get_currentSongFreeplayPreviewStart():Int + { + return currentSongMetadata.playData.previewStart; + } + + function set_currentSongFreeplayPreviewStart(value:Int):Int + { + return currentSongMetadata.playData.previewStart = value; + } + + var currentSongFreeplayPreviewEnd(get, set):Int; + + function get_currentSongFreeplayPreviewEnd():Int + { + return currentSongMetadata.playData.previewEnd; + } + + function set_currentSongFreeplayPreviewEnd(value:Int):Int + { + return currentSongMetadata.playData.previewEnd = value; + } + var currentSongStage(get, set):String; function get_currentSongStage():String @@ -2920,6 +2946,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemToggleToolboxOffsets.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT, event.value); menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value); menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value); + menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value); menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value); menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value); menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value); @@ -5960,6 +5987,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + function hardRefreshFreeplayToolbox():Void + { + var freeplayToolbox:ChartEditorFreeplayToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT); + if (freeplayToolbox != null) + { + freeplayToolbox.refreshAudioPreview(); + freeplayToolbox.refresh(); + } + } + /** * Clear the voices group. */ diff --git a/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx b/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx index aef402244..ca1fda6b9 100644 --- a/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx @@ -52,7 +52,11 @@ class SetAudioOffsetCommand implements ChartEditorCommand } // Update the offsets toolbox. - if (refreshOffsetsToolbox) state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT); + if (refreshOffsetsToolbox) + { + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT); + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT); + } } public function undo(state:ChartEditorState):Void diff --git a/source/funkin/ui/debug/charting/commands/SetFreeplayPreviewCommand.hx b/source/funkin/ui/debug/charting/commands/SetFreeplayPreviewCommand.hx new file mode 100644 index 000000000..232768904 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/SetFreeplayPreviewCommand.hx @@ -0,0 +1,62 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Command that sets the start time or end time of the Freeplay preview. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class SetFreeplayPreviewCommand implements ChartEditorCommand +{ + var previousStartTime:Int = 0; + var previousEndTime:Int = 0; + var newStartTime:Null = null; + var newEndTime:Null = null; + + public function new(newStartTime:Null, newEndTime:Null) + { + this.newStartTime = newStartTime; + this.newEndTime = newEndTime; + } + + public function execute(state:ChartEditorState):Void + { + this.previousStartTime = state.currentSongFreeplayPreviewStart; + this.previousEndTime = state.currentSongFreeplayPreviewEnd; + + if (newStartTime != null) state.currentSongFreeplayPreviewStart = newStartTime; + if (newEndTime != null) state.currentSongFreeplayPreviewEnd = newEndTime; + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongFreeplayPreviewStart = previousStartTime; + state.currentSongFreeplayPreviewEnd = previousEndTime; + } + + public function shouldAddToHistory(state:ChartEditorState):Bool + { + return (newStartTime != null && newStartTime != previousStartTime) || (newEndTime != null && newEndTime != previousEndTime); + } + + public function toString():String + { + var setStart = newStartTime != null && newStartTime != previousStartTime; + var setEnd = newEndTime != null && newEndTime != previousEndTime; + + if (setStart && !setEnd) + { + return "Set Freeplay Preview Start Time"; + } + else if (setEnd && !setStart) + { + return "Set Freeplay Preview End Time"; + } + else + { + return "Set Freeplay Preview Start and End Times"; + } + } +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 8e40cfc42..76b2a388e 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -128,17 +128,42 @@ class ChartEditorAudioHandler public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool { + var perfA = haxe.Timer.stamp(); + var result:Bool = playInstrumental(state, instId); if (!result) return false; + var perfB = haxe.Timer.stamp(); + stopExistingVocals(state); + + var perfC = haxe.Timer.stamp(); + result = playVocals(state, BF, playerId, instId); + + var perfD = haxe.Timer.stamp(); + // if (!result) return false; result = playVocals(state, DAD, opponentId, instId); // if (!result) return false; + var perfE = haxe.Timer.stamp(); + state.hardRefreshOffsetsToolbox(); + var perfF = haxe.Timer.stamp(); + + state.hardRefreshFreeplayToolbox(); + + var perfG = haxe.Timer.stamp(); + + trace('Switched to instrumental in ${perfB - perfA} seconds.'); + trace('Stopped existing vocals in ${perfC - perfB} seconds.'); + trace('Played BF vocals in ${perfD - perfC} seconds.'); + trace('Played DAD vocals in ${perfE - perfD} seconds.'); + trace('Hard refreshed offsets toolbox in ${perfF - perfE} seconds.'); + trace('Hard refreshed freeplay toolbox in ${perfG - perfF} seconds.'); + return true; } @@ -149,7 +174,10 @@ class ChartEditorAudioHandler { if (instId == '') instId = 'default'; var instTrackData:Null = state.audioInstTrackData.get(instId); + var perfA = haxe.Timer.stamp(); var instTrack:Null = SoundUtil.buildSoundFromBytes(instTrackData); + var perfB = haxe.Timer.stamp(); + trace('Built instrumental track in ${perfB - perfA} seconds.'); if (instTrack == null) return false; stopExistingInstrumental(state); @@ -177,7 +205,10 @@ class ChartEditorAudioHandler { var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; var vocalTrackData:Null = state.audioVocalTrackData.get(trackId); + var perfStart = haxe.Timer.stamp(); var vocalTrack:Null = SoundUtil.buildSoundFromBytes(vocalTrackData); + var perfEnd = haxe.Timer.stamp(); + trace('Built vocal track in ${perfEnd - perfStart} seconds.'); if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup(); @@ -188,7 +219,11 @@ class ChartEditorAudioHandler case BF: state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); - var waveformData:Null = WaveformDataParser.interpretFlxSound(vocalTrack); + var perfStart = haxe.Timer.stamp(); + var waveformData:Null = vocalTrack.waveformData; + var perfEnd = haxe.Timer.stamp(); + trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.'); + if (waveformData != null) { var duration:Float = Conductor.instance.getStepTimeInMs(16) * 0.001; @@ -211,7 +246,11 @@ class ChartEditorAudioHandler case DAD: state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); - var waveformData:Null = WaveformDataParser.interpretFlxSound(vocalTrack); + var perfStart = haxe.Timer.stamp(); + var waveformData:Null = vocalTrack.waveformData; + var perfEnd = haxe.Timer.stamp(); + trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.'); + if (waveformData != null) { var duration:Float = Conductor.instance.getStepTimeInMs(16) * 0.001; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 04d89e3f4..0318bf296 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -28,6 +28,8 @@ class ChartEditorImportExportHandler */ public static function loadSongAsTemplate(state:ChartEditorState, songId:String):Void { + trace('===============START'); + var song:Null = SongRegistry.instance.fetchEntry(songId); if (song == null) return; @@ -98,11 +100,14 @@ class ChartEditorImportExportHandler state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); + state.postLoadInstrumental(); state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); state.success('Success', 'Loaded song (${rawSongMetadata[0].songName})'); + + trace('===============END'); } /** diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index b246e653f..9e22ba833 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -36,6 +36,7 @@ import haxe.ui.containers.dialogs.Dialog.DialogEvent; import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; +import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; import haxe.ui.containers.Frame; @@ -92,6 +93,8 @@ class ChartEditorToolboxHandler cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT: cast(toolbox, ChartEditorBaseToolbox).refresh(); + case ChartEditorState.CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT: + cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: onShowToolboxPlayerPreview(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: @@ -205,6 +208,8 @@ class ChartEditorToolboxHandler toolbox = buildToolboxMetadataLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT: toolbox = buildToolboxOffsetsLayout(state); + case ChartEditorState.CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT: + toolbox = buildToolboxFreeplayLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: toolbox = buildToolboxPlayerPreviewLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: @@ -383,6 +388,15 @@ class ChartEditorToolboxHandler return toolbox; } + static function buildToolboxFreeplayLayout(state:ChartEditorState):Null + { + var toolbox:ChartEditorBaseToolbox = ChartEditorFreeplayToolbox.build(state); + + if (toolbox == null) return null; + + return toolbox; + } + static function buildToolboxEventDataLayout(state:ChartEditorState):Null { var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state); diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx new file mode 100644 index 000000000..8d3554a08 --- /dev/null +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx @@ -0,0 +1,693 @@ +package funkin.ui.debug.charting.toolboxes; + +import funkin.audio.SoundGroup; +import haxe.ui.components.Button; +import haxe.ui.components.HorizontalSlider; +import haxe.ui.components.Label; +import flixel.addons.display.FlxTiledSprite; +import flixel.math.FlxMath; +import haxe.ui.components.NumberStepper; +import haxe.ui.components.Slider; +import haxe.ui.backend.flixel.components.SpriteWrapper; +import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand; +import funkin.ui.haxeui.components.WaveformPlayer; +import funkin.audio.waveform.WaveformDataParser; +import haxe.ui.containers.VBox; +import haxe.ui.containers.Absolute; +import haxe.ui.containers.ScrollView; +import funkin.ui.freeplay.FreeplayState; +import haxe.ui.containers.Frame; +import haxe.ui.core.Screen; +import haxe.ui.events.DragEvent; +import haxe.ui.events.MouseEvent; +import haxe.ui.events.UIEvent; + +/** + * The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM. + */ +// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/freeplay.xml")) +class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox +{ + var waveformContainer:Absolute; + var waveformScrollview:ScrollView; + var waveformMusic:WaveformPlayer; + var freeplayButtonZoomIn:Button; + var freeplayButtonZoomOut:Button; + var freeplayButtonPause:Button; + var freeplayButtonPlay:Button; + var freeplayButtonStop:Button; + var freeplayPreviewStart:NumberStepper; + var freeplayPreviewEnd:NumberStepper; + var freeplayTicksContainer:Absolute; + var playheadSprite:SpriteWrapper; + var previewSelectionSprite:SpriteWrapper; + + static final TICK_LABEL_X_OFFSET:Float = 4.0; + + static final PLAYHEAD_RIGHT_PAD:Float = 10.0; + + static final BASE_SCALE:Float = 64.0; + static final STARTING_SCALE:Float = 1024.0; + static final MIN_SCALE:Float = 4.0; + static final WAVEFORM_ZOOM_MULT:Float = 1.5; + + static final MAGIC_SCALE_BASE_TIME:Float = 5.0; + + var waveformScale:Float = STARTING_SCALE; + + var playheadAbsolutePos(get, set):Float; + + function get_playheadAbsolutePos():Float + { + return playheadSprite.left; + } + + function set_playheadAbsolutePos(value:Float):Float + { + return playheadSprite.left = value; + } + + var playheadRelativePos(get, set):Float; + + function get_playheadRelativePos():Float + { + return playheadSprite.left - waveformScrollview.hscrollPos; + } + + function set_playheadRelativePos(value:Float):Float + { + return playheadSprite.left = waveformScrollview.hscrollPos + value; + } + + var previewBoxStartPosAbsolute(get, set):Float; + + function get_previewBoxStartPosAbsolute():Float + { + return previewSelectionSprite.left; + } + + function set_previewBoxStartPosAbsolute(value:Float):Float + { + return previewSelectionSprite.left = value; + } + + var previewBoxEndPosAbsolute(get, set):Float; + + function get_previewBoxEndPosAbsolute():Float + { + return previewSelectionSprite.left + previewSelectionSprite.width; + } + + function set_previewBoxEndPosAbsolute(value:Float):Float + { + if (value < previewBoxStartPosAbsolute) return previewSelectionSprite.left = previewBoxStartPosAbsolute; + return previewSelectionSprite.width = value - previewBoxStartPosAbsolute; + } + + var previewBoxStartPosRelative(get, set):Float; + + function get_previewBoxStartPosRelative():Float + { + return previewSelectionSprite.left - waveformScrollview.hscrollPos; + } + + function set_previewBoxStartPosRelative(value:Float):Float + { + return previewSelectionSprite.left = waveformScrollview.hscrollPos + value; + } + + var previewBoxEndPosRelative(get, set):Float; + + function get_previewBoxEndPosRelative():Float + { + return previewSelectionSprite.left + previewSelectionSprite.width - waveformScrollview.hscrollPos; + } + + function set_previewBoxEndPosRelative(value:Float):Float + { + if (value < previewBoxStartPosRelative) return previewSelectionSprite.left = previewBoxStartPosRelative; + return previewSelectionSprite.width = value - previewBoxStartPosRelative; + } + + /** + * The amount you need to multiply the zoom by such that, at the base zoom level, one tick is equal to `MAGIC_SCALE_BASE_TIME` seconds. + */ + var waveformMagicFactor:Float = 1.0; + + var audioPreviewTracks:SoundGroup; + + var tickTiledSprite:FlxTiledSprite; + + var freeplayPreviewVolume(get, null):Float; + + function get_freeplayPreviewVolume():Float + { + return freeplayMusicVolume.value * 2 / 100; + } + + var tickLabels:Array