From 4bf9f686586e03b197312afdb824b48dd3455bbb Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Fri, 9 Feb 2024 14:58:57 -0500 Subject: [PATCH] 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<FunkinSound> 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<WaveformData> = 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<FunkinSound> // 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<FunkinSound> 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<FunkinSound> }); } + /** + * 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<FunkinSound> 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<WaveformData> + public function getPlayerVoiceWaveform():Null<WaveformData> { if (playerVoices.members.length == 0) return null; - return WaveformDataParser.interpretFlxSound(playerVoices.members[0]); + return playerVoices.members[0].waveformData; } - public function buildOpponentVoiceWaveform():Null<WaveformData> + public function getOpponentVoiceWaveform():Null<WaveformData> { 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<Int> = []; + 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<SongPlayData> @:optional public var album:Null<String>; + /** + * 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<String, Int>(); @@ -438,6 +456,7 @@ class SongPlayData implements ICloneable<SongPlayData> 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<SongPlayData> 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<Song, SongMetadata> * 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<String> = ['ogg']; + public static final SUPPORTED_MUSIC_FORMATS:Array<String> = #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<Int> = null; + var newEndTime:Null<Int> = null; + + public function new(newStartTime:Null<Int>, newEndTime:Null<Int>) + { + 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<Bytes> = state.audioInstTrackData.get(instId); + var perfA = haxe.Timer.stamp(); var instTrack:Null<FunkinSound> = 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<Bytes> = state.audioVocalTrackData.get(trackId); + var perfStart = haxe.Timer.stamp(); var vocalTrack:Null<FunkinSound> = 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<WaveformData> = WaveformDataParser.interpretFlxSound(vocalTrack); + var perfStart = haxe.Timer.stamp(); + var waveformData:Null<WaveformData> = 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<WaveformData> = WaveformDataParser.interpretFlxSound(vocalTrack); + var perfStart = haxe.Timer.stamp(); + var waveformData:Null<WaveformData> = 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<Song> = 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<ChartEditorBaseToolbox> + { + var toolbox:ChartEditorBaseToolbox = ChartEditorFreeplayToolbox.build(state); + + if (toolbox == null) return null; + + return toolbox; + } + static function buildToolboxEventDataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox> { 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<Label> = []; + + public function new(chartEditorState2:ChartEditorState) + { + super(chartEditorState2); + + initialize(); + + this.onDialogClosed = onClose; + } + + function onClose(event:UIEvent) + { + chartEditorState.menubarItemToggleToolboxFreeplay.selected = false; + } + + function initialize():Void + { + // Starting position. + // TODO: Save and load this. + this.x = 150; + this.y = 250; + + freeplayMusicVolume.onChange = (_) -> { + setTrackVolume(freeplayPreviewVolume); + }; + freeplayMusicMute.onClick = (_) -> { + toggleMuteTrack(); + }; + freeplayButtonZoomIn.onClick = (_) -> { + zoomWaveformIn(); + }; + freeplayButtonZoomOut.onClick = (_) -> { + zoomWaveformOut(); + }; + freeplayButtonPause.onClick = (_) -> { + pauseAudioPreview(); + }; + freeplayButtonPlay.onClick = (_) -> { + playAudioPreview(); + }; + freeplayButtonStop.onClick = (_) -> { + stopAudioPreview(); + }; + testPreview.onClick = (_) -> { + performPreview(); + }; + freeplayPreviewStart.onChange = (event:UIEvent) -> { + if (event.value == chartEditorState.currentSongFreeplayPreviewStart) return; + if (waveformDragStartPos != null) return; // The values are changing because we are dragging the preview. + + chartEditorState.performCommand(new SetFreeplayPreviewCommand(event.value, null)); + refresh(); + } + freeplayPreviewEnd.onChange = (event:UIEvent) -> { + if (event.value == chartEditorState.currentSongFreeplayPreviewEnd) return; + if (waveformDragStartPos != null) return; // The values are changing because we are dragging the preview. + + chartEditorState.performCommand(new SetFreeplayPreviewCommand(null, event.value)); + refresh(); + } + waveformScrollview.onScroll = (_) -> { + if (!audioPreviewTracks.playing) + { + // Move the playhead if it would go out of view. + var prevPlayheadRelativePos = playheadRelativePos; + playheadRelativePos = FlxMath.bound(playheadRelativePos, 0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD); + trace('newPos: ${playheadRelativePos}'); + var diff = playheadRelativePos - prevPlayheadRelativePos; + + if (diff != 0) + { + // We have to change the song time to match the playhead position when we move it. + var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor)); + var targetSongTimeSeconds:Float = waveformMusic.waveform.waveformData.indexToSeconds(currentWaveformIndex); + audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC; + } + + addOffsetsToAudioPreview(); + } + else + { + // The scrollview probably changed because the song position changed. + // If we try to move the song now it will glitch. + } + + // Either way, clipRect has changed, so we need to refresh the waveforms. + refresh(); + }; + + initializeTicks(); + + refreshAudioPreview(); + refresh(); + refreshTicks(); + + waveformMusic.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(); + }); + + freeplayTicksContainer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragPlayhead(); + }); + } + + function initializeTicks():Void + { + tickTiledSprite = new FlxTiledSprite(chartEditorState.offsetTickBitmap, 100, chartEditorState.offsetTickBitmap.height, true, false); + freeplayTicksSprite.sprite = tickTiledSprite; + tickTiledSprite.width = 5000; + } + + /** + * Pull the audio tracks from the chart editor state and create copies of them to play in the Offsets Toolbox. + * These must be DEEP CLONES or else the editor will affect the audio preview! + */ + public function refreshAudioPreview():Void + { + if (audioPreviewTracks == null) + { + audioPreviewTracks = new SoundGroup(); + // Make sure audioPreviewTracks (and all its children) receives update() calls. + chartEditorState.add(audioPreviewTracks); + } + else + { + audioPreviewTracks.stop(); + audioPreviewTracks.clear(); + } + + var instTrack = chartEditorState.audioInstTrack.clone(); + audioPreviewTracks.add(instTrack); + + var playerVoice = chartEditorState.audioVocalTrackGroup.getPlayerVoice(); + if (playerVoice != null) audioPreviewTracks.add(playerVoice.clone()); + + var opponentVoice = chartEditorState.audioVocalTrackGroup.getOpponentVoice(); + if (opponentVoice != null) audioPreviewTracks.add(opponentVoice.clone()); + + // Build player waveform. + // waveformMusic.waveform.forceUpdate = true; + var perfStart = haxe.Timer.stamp(); + var waveformData1 = playerVoice.waveformData; + var waveformData2 = opponentVoice.waveformData; + var waveformData3 = chartEditorState.audioInstTrack.waveformData; + var waveformData = waveformData1.merge(waveformData2).merge(waveformData3); + trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds'); + + waveformMusic.waveform.waveformData = waveformData; + // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it. + waveformMusic.waveform.duration = instTrack.length / Constants.MS_PER_SEC; + + addOffsetsToAudioPreview(); + } + + public function refreshTicks():Void + { + while (tickLabels.length > 0) + { + var label = tickLabels.pop(); + freeplayTicksContainer.removeComponent(label); + } + + var labelYPos:Float = chartEditorState.offsetTickBitmap.height / 2; + var labelHeight:Float = chartEditorState.offsetTickBitmap.height / 2; + + var numberOfTicks:Int = Math.floor(waveformMusic.waveform.width / chartEditorState.offsetTickBitmap.width * 2) + 1; + + for (index in 0...numberOfTicks) + { + var tickPos = chartEditorState.offsetTickBitmap.width / 2 * index; + var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformMusic.waveform.waveformData.pointsPerSecond(); + + var tickLabel:Label = new Label(); + tickLabel.text = formatTime(tickTime); + tickLabel.styleNames = "offset-ticks-label"; + tickLabel.height = labelHeight; + // Positioning within offsetTicksContainer is absolute (relative to the container itself). + tickLabel.top = labelYPos; + tickLabel.left = tickPos + TICK_LABEL_X_OFFSET; + + freeplayTicksContainer.addComponent(tickLabel); + tickLabels.push(tickLabel); + } + } + + function formatTime(seconds:Float):String + { + if (seconds <= 0) return "0.0"; + + var integerSeconds = Math.floor(seconds); + var decimalSeconds = Math.floor((seconds - integerSeconds) * 10); + + if (integerSeconds < 60) + { + return '${integerSeconds}.${decimalSeconds}'; + } + else + { + var integerMinutes = Math.floor(integerSeconds / 60); + var remainingSeconds = integerSeconds % 60; + var remainingSecondsPad:String = remainingSeconds < 10 ? '0$remainingSeconds' : '$remainingSeconds'; + + return '${integerMinutes}:${remainingSecondsPad}${decimalSeconds > 0 ? '.$decimalSeconds' : ''}'; + } + } + + function buildTickLabel():Void {} + + public function onStartDragPlayhead():Void + { + Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead); + Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead); + + movePlayheadToMouse(); + } + + public function onDragPlayhead(event:MouseEvent):Void + { + movePlayheadToMouse(); + } + + public function onStopDragPlayhead(event:MouseEvent):Void + { + // Stop dragging. + Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead); + Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead); + } + + function movePlayheadToMouse():Void + { + // Determine the position of the mouse relative to the + var mouseXPos = FlxG.mouse.x; + + var relativeMouseXPos = mouseXPos - waveformScrollview.screenX; + var targetPlayheadPos = relativeMouseXPos + waveformScrollview.hscrollPos; + + // Move the playhead to the mouse position. + playheadAbsolutePos = targetPlayheadPos; + + // Move the audio preview to the playhead position. + var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor)); + var targetSongTimeSeconds:Float = waveformMusic.waveform.waveformData.indexToSeconds(currentWaveformIndex); + audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC; + } + + var waveformDragStartPos:Null<Float> = null; + + var waveformDragPreviewStartPos:Float; + var waveformDragPreviewEndPos:Float; + + public function onStartDragWaveform():Void + { + waveformDragStartPos = FlxG.mouse.x; + + Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragWaveform); + Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragWaveform); + } + + public function onDragWaveform(event:MouseEvent):Void + { + // Set waveformDragPreviewStartPos and waveformDragPreviewEndPos to the position the drag started and the current mouse position. + // This only affects the visuals. + + var currentAbsMousePos = FlxG.mouse.x; + var dragDiff = currentAbsMousePos - waveformDragStartPos; + + var currentRelativeMousePos = currentAbsMousePos - waveformScrollview.screenX; + var relativeStartPos = waveformDragStartPos - waveformScrollview.screenX; + + var isDraggingRight = dragDiff > 0; + var hasDraggedEnough = Math.abs(dragDiff) > 10; + + if (hasDraggedEnough) + { + if (isDraggingRight) + { + waveformDragPreviewStartPos = relativeStartPos; + waveformDragPreviewEndPos = currentRelativeMousePos; + } + else + { + waveformDragPreviewStartPos = currentRelativeMousePos; + waveformDragPreviewEndPos = relativeStartPos; + } + } + + refresh(); + } + + public function onStopDragWaveform(event:MouseEvent):Void + { + Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragWaveform); + Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragWaveform); + + var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos; + var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); + var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC); + + var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos; + var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); + var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC); + + chartEditorState.performCommand(new SetFreeplayPreviewCommand(previewStartPosMs, previewEndPosMs)); + + waveformDragStartPos = null; + waveformDragPreviewStartPos = 0; + waveformDragPreviewEndPos = 0; + + refresh(); + addOffsetsToAudioPreview(); + } + + public function playAudioPreview():Void + { + if (isPerformingPreview) stopPerformingPreview(); + + audioPreviewTracks.volume = freeplayPreviewVolume; + audioPreviewTracks.play(false, audioPreviewTracks.time); + } + + public function addOffsetsToAudioPreview():Void + { + var trackInst = audioPreviewTracks.members[0]; + if (trackInst != null) + { + trackInst.time -= chartEditorState.currentInstrumentalOffset; + } + + var trackPlayer = audioPreviewTracks.members[1]; + if (trackPlayer != null) + { + trackPlayer.time -= chartEditorState.currentVocalOffsetPlayer; + } + + var trackOpponent = audioPreviewTracks.members[2]; + if (trackOpponent != null) + { + trackOpponent.time -= chartEditorState.currentVocalOffsetOpponent; + } + } + + public function pauseAudioPreview():Void + { + if (isPerformingPreview) stopPerformingPreview(); + + audioPreviewTracks.pause(); + } + + public function stopAudioPreview():Void + { + if (isPerformingPreview) stopPerformingPreview(); + + audioPreviewTracks.stop(); + + audioPreviewTracks.time = 0; + + waveformScrollview.hscrollPos = 0; + playheadAbsolutePos = 0 + playheadSprite.width; + refresh(); + addOffsetsToAudioPreview(); + } + + public function zoomWaveformIn():Void + { + if (isPerformingPreview) stopPerformingPreview(); + + if (waveformScale > MIN_SCALE) + { + waveformScale = waveformScale / WAVEFORM_ZOOM_MULT; + if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE; + + trace('Zooming in, scale: ${waveformScale}'); + + // Update the playhead too! + playheadAbsolutePos = playheadAbsolutePos * WAVEFORM_ZOOM_MULT; + + // Recenter the scroll view on the playhead. + var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8; + waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset; + + refresh(); + refreshTicks(); + } + else + { + waveformScale = MIN_SCALE; + } + } + + public function zoomWaveformOut():Void + { + waveformScale = waveformScale * WAVEFORM_ZOOM_MULT; + if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE; + + trace('Zooming out, scale: ${waveformScale}'); + + // Update the playhead too! + playheadAbsolutePos = playheadAbsolutePos / WAVEFORM_ZOOM_MULT; + + // Recenter the scroll view on the playhead. + var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8; + waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset; + + refresh(); + refreshTicks(); + } + + public function setTrackVolume(volume:Float):Void + { + audioPreviewTracks.volume = volume; + } + + public function muteTrack():Void + { + audioPreviewTracks.muted = true; + } + + public function unmuteTrack():Void + { + audioPreviewTracks.muted = false; + } + + public function toggleMuteTrack():Void + { + audioPreviewTracks.muted = !audioPreviewTracks.muted; + } + + var isPerformingPreview:Bool = false; + var isFadingOutPreview:Bool = false; + + public function performPreview():Void + { + isPerformingPreview = true; + isFadingOutPreview = false; + audioPreviewTracks.play(true, chartEditorState.currentSongFreeplayPreviewStart); + audioPreviewTracks.fadeIn(FreeplayState.FADE_IN_DURATION, FreeplayState.FADE_IN_START_VOLUME * freeplayPreviewVolume, + FreeplayState.FADE_IN_END_VOLUME * freeplayPreviewVolume, null); + } + + public function stopPerformingPreview():Void + { + isPerformingPreview = false; + isFadingOutPreview = false; + audioPreviewTracks.volume = freeplayPreviewVolume; + audioPreviewTracks.pause(); + } + + public override function update(elapsed:Float) + { + super.update(elapsed); + + if (isPerformingPreview && !audioPreviewTracks.playing) + { + stopPerformingPreview(); + } + + if (isPerformingPreview && audioPreviewTracks.playing) + { + var startFadeOutTime = chartEditorState.currentSongFreeplayPreviewEnd - (FreeplayState.FADE_OUT_DURATION * Constants.MS_PER_SEC); + trace('startFadeOutTime: ${audioPreviewTracks.time} >= ${startFadeOutTime}'); + if (!isFadingOutPreview && audioPreviewTracks.time >= startFadeOutTime) + { + isFadingOutPreview = true; + audioPreviewTracks.fadeOut(FreeplayState.FADE_OUT_DURATION, FreeplayState.FADE_OUT_END_VOLUME * freeplayPreviewVolume, (_) -> { + trace('Stop performing preview! ${audioPreviewTracks.time}'); + stopPerformingPreview(); + }); + } + } + + if (audioPreviewTracks.playing) + { + var targetScrollPos:Float = waveformMusic.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + // waveformScrollview.hscrollPos = targetScrollPos; + var prevPlayheadAbsolutePos = playheadAbsolutePos; + playheadAbsolutePos = targetScrollPos; + var playheadDiff = playheadAbsolutePos - prevPlayheadAbsolutePos; + + // BEHAVIOR C. + // Copy Audacity! + // If the playhead is out of view, jump forward or backward by one screen width until it's in view. + if (playheadAbsolutePos < waveformScrollview.hscrollPos) + { + waveformScrollview.hscrollPos -= waveformScrollview.width; + } + if (playheadAbsolutePos > waveformScrollview.hscrollPos + waveformScrollview.width) + { + waveformScrollview.hscrollPos += waveformScrollview.width; + } + } + freeplayLabelTime.text = formatTime(audioPreviewTracks.time / Constants.MS_PER_SEC); + if (waveformDragStartPos != null && (waveformDragPreviewStartPos > 0 && waveformDragPreviewEndPos > 0)) + { + var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos; + var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); + var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC); + + var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos; + var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor)); + var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC); + + // Set the values in milliseconds. + freeplayPreviewStart.value = previewStartPosMs; + freeplayPreviewEnd.value = previewEndPosMs; + + previewBoxStartPosAbsolute = previewStartPosAbsolute; + previewBoxEndPosAbsolute = previewEndPosAbsolute; + } + else + { + previewBoxStartPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewStart / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + previewBoxEndPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewEnd / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor); + + freeplayPreviewStart.value = chartEditorState.currentSongFreeplayPreviewStart; + freeplayPreviewEnd.value = chartEditorState.currentSongFreeplayPreviewEnd; + } + } + + public override function refresh():Void + { + super.refresh(); + + waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformMusic.waveform.waveformData.pointsPerSecond()); + + var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor; + + var maxWidth:Int = -1; + + waveformMusic.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC; + waveformMusic.waveform.width = (waveformMusic.waveform.waveformData?.length ?? 1000) / currentZoomFactor; + if (waveformMusic.waveform.width > maxWidth) maxWidth = Std.int(waveformMusic.waveform.width); + waveformMusic.waveform.height = 65; + waveformMusic.waveform.markDirty(); + + waveformContainer.width = maxWidth; + tickTiledSprite.width = maxWidth; + } + + public static function build(chartEditorState:ChartEditorState):ChartEditorFreeplayToolbox + { + return new ChartEditorFreeplayToolbox(chartEditorState); + } +} diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index 5b26419b2..67ca82b1b 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -191,7 +191,6 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox // Move the playhead if it would go out of view. var prevPlayheadRelativePos = playheadRelativePos; playheadRelativePos = FlxMath.bound(playheadRelativePos, 0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD); - trace('newPos: ${playheadRelativePos}'); var diff = playheadRelativePos - prevPlayheadRelativePos; if (diff != 0) @@ -271,18 +270,18 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox // Build player waveform. // waveformPlayer.waveform.forceUpdate = true; - waveformPlayer.waveform.waveformData = WaveformDataParser.interpretFlxSound(playerVoice); + waveformPlayer.waveform.waveformData = playerVoice.waveformData; // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it. waveformPlayer.waveform.duration = playerVoice.length / Constants.MS_PER_SEC; // Build opponent waveform. // waveformOpponent.waveform.forceUpdate = true; - waveformOpponent.waveform.waveformData = WaveformDataParser.interpretFlxSound(opponentVoice); + waveformOpponent.waveform.waveformData = opponentVoice.waveformData; waveformOpponent.waveform.duration = opponentVoice.length / Constants.MS_PER_SEC; // Build instrumental waveform. // waveformInstrumental.waveform.forceUpdate = true; - waveformInstrumental.waveform.waveformData = WaveformDataParser.interpretFlxSound(instTrack); + waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData; waveformInstrumental.waveform.duration = instTrack.length / Constants.MS_PER_SEC; addOffsetsToAudioPreview(); @@ -410,8 +409,6 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox deltaPixels / waveformInstrumental.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; }; - trace('Moving waveform by ${deltaMousePosition} -> ${deltaPixels} -> ${deltaMilliseconds} milliseconds.'); - switch (dragWaveform) { case PLAYER: @@ -537,8 +534,6 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox waveformScale = waveformScale / WAVEFORM_ZOOM_MULT; if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE; - trace('Zooming in, scale: ${waveformScale}'); - // Update the playhead too! playheadAbsolutePos = playheadAbsolutePos * WAVEFORM_ZOOM_MULT; @@ -560,8 +555,6 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox waveformScale = waveformScale * WAVEFORM_ZOOM_MULT; if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE; - trace('Zooming out, scale: ${waveformScale}'); - // Update the playhead too! playheadAbsolutePos = playheadAbsolutePos / WAVEFORM_ZOOM_MULT; @@ -776,7 +769,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; } } - + offsetLabelTime.text = formatTime(audioPreviewTracks.time / Constants.MS_PER_SEC); // Keep the playhead in view. // playheadRelativePos = FlxMath.bound(playheadRelativePos, waveformScrollview.hscrollPos + 1, // Math.min(waveformScrollview.hscrollPos + waveformScrollview.width, waveformContainer.width)); diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index de6484fd3..b23ca6e54 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -56,6 +56,31 @@ import lime.utils.Assets; class FreeplayState extends MusicBeatSubState { + /** + * For the audio preview, the duration of the fade-in effect. + */ + public static final FADE_IN_DURATION:Float = 0.5; + + /** + * For the audio preview, the duration of the fade-out effect. + */ + public static final FADE_OUT_DURATION:Float = 0.25; + + /** + * For the audio preview, the volume at which the fade-in starts. + */ + public static final FADE_IN_START_VOLUME:Float = 0.25; + + /** + * For the audio preview, the volume at which the fade-in ends. + */ + public static final FADE_IN_END_VOLUME:Float = 1.0; + + /** + * For the audio preview, the volume at which the fade-out starts. + */ + public static final FADE_OUT_END_VOLUME:Float = 0.0; + var songs:Array<Null<FreeplaySongData>> = []; var diffIdsCurrent:Array<String> = [];