From f6e4bc863d135d40acfb3234dc6592ef35f639cb Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 27 Jan 2024 03:24:49 -0500 Subject: [PATCH] Work in progress on offsets toolbox (working dragging!) --- assets | 2 +- source/funkin/audio/FunkinSound.hx | 25 +- source/funkin/audio/SoundGroup.hx | 9 + source/funkin/audio/VoicesGroup.hx | 10 + .../funkin/audio/waveform/WaveformSprite.hx | 8 + .../ui/debug/charting/ChartEditorState.hx | 21 +- .../handlers/ChartEditorAudioHandler.hx | 8 +- .../toolboxes/ChartEditorMetadataToolbox.hx | 23 -- .../toolboxes/ChartEditorOffsetsToolbox.hx | 331 ++++++++++++++++-- source/funkin/util/logging/CrashHandler.hx | 18 + 10 files changed, 395 insertions(+), 60 deletions(-) diff --git a/assets b/assets index ae904d08d..384a99d73 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ae904d08df53712a582d5a3c26de40ef552422d1 +Subproject commit 384a99d732456b2b3d35fa9bd2e10aa5f747afa6 diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index acae9c638..2424aca13 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -7,6 +7,7 @@ import flash.utils.ByteArray; import flixel.sound.FlxSound; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.system.FlxAssets.FlxSoundAsset; +import funkin.util.tools.ICloneable; import openfl.Assets; #if (openfl >= "8.0.0") import openfl.utils.AssetType; @@ -17,10 +18,17 @@ import openfl.utils.AssetType; * - Delayed playback via negative song position. */ @:nullSafety -class FunkinSound extends FlxSound +class FunkinSound extends FlxSound implements ICloneable { static var cache(default, null):FlxTypedGroup = new FlxTypedGroup(); + public var paused(get, never):Bool; + + function get_paused():Bool + { + return this._paused; + } + public var isPlaying(get, never):Bool; function get_isPlaying():Bool @@ -153,6 +161,21 @@ class FunkinSound extends FlxSound return this; } + public function clone():FunkinSound + { + var sound:FunkinSound = new FunkinSound(); + + // Clone the sound by creating one with the same data buffer. + // Reusing the `Sound` object directly causes issues with playback. + @:privateAccess + sound._sound = openfl.media.Sound.fromAudioBuffer(this._sound.__buffer); + + // Call init to ensure the FlxSound is properly initialized. + sound.init(this.looped, this.autoDestroy, this.onComplete); + + return sound; + } + /** * Creates a new `FunkinSound` object. * diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 528aaa80c..64104fee7 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -16,6 +16,8 @@ class SoundGroup extends FlxTypedGroup public var pitch(get, set):Float; + public var playing(get, never):Bool; + public function new() { super(); @@ -165,6 +167,13 @@ class SoundGroup extends FlxTypedGroup return time; } + function get_playing():Bool + { + if (getFirstAlive != null) return getFirstAlive().playing; + else + return false; + } + function get_volume():Float { if (getFirstAlive() != null) return getFirstAlive().volume; diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index c47b775b4..70a01f9dc 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -106,6 +106,16 @@ class VoicesGroup extends SoundGroup return opponentVolume = volume; } + public function getPlayerVoice(index:Int = 0):Null + { + return playerVoices.members[index]; + } + + public function getOpponentVoice(index:Int = 0):Null + { + return opponentVoices.members[index]; + } + public function buildPlayerVoiceWaveform():Null { if (playerVoices.members.length == 0) return null; diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index ceeab7150..32ced2fbd 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -145,6 +145,14 @@ class WaveformSprite extends MeshRender this.forceUpdate = false; } + /** + * Manually tell the waveform to rebuild itself, even if none of its properties have changed. + */ + public function markDirty():Void + { + isWaveformDirty = true; + } + public override function update(elapsed:Float) { super.update(elapsed); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a7d6976cd..ec6d36b07 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1438,19 +1438,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return value; } - var currentVocalOffset(get, set):Float; + var currentVocalOffsetPlayer(get, set):Float; - function get_currentVocalOffset():Float + function get_currentVocalOffsetPlayer():Float { - // Currently there's only one vocal offset, so we just grab the player's offset since both should be the same. - // Should probably make it so we can set offsets for player + opponent individually, though. return currentSongOffsets.getVocalOffset(currentPlayerChar); } - function set_currentVocalOffset(value:Float):Float + function set_currentVocalOffsetPlayer(value:Float):Float { - // Currently there's only one vocal offset, so we just apply it to both characters. currentSongOffsets.setVocalOffset(currentPlayerChar, value); + return value; + } + + var currentVocalOffsetOpponent(get, set):Float; + + function get_currentVocalOffsetOpponent():Float + { + return currentSongOffsets.getVocalOffset(currentOpponentChar); + } + + function set_currentVocalOffsetOpponent(value:Float):Float + { currentSongOffsets.setVocalOffset(currentOpponentChar, value); return value; } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 8f60a0780..7f0a1e155 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -204,7 +204,7 @@ class ChartEditorAudioHandler trace('[WARN] Failed to parse waveform data for vocal track.'); } - state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffset; + state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer; return true; case DAD: state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); @@ -227,7 +227,7 @@ class ChartEditorAudioHandler trace('[WARN] Failed to parse waveform data for vocal track.'); } - state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffset; + state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent; return true; case OTHER: @@ -248,9 +248,9 @@ class ChartEditorAudioHandler { state.audioVocalTrackGroup.clear(); } - if (state.audioVisGroup != null) + if (state.audioWaveforms != null) { - state.audioVisGroup.clearAllVis(); + state.audioWaveforms.clear(); } } diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx index 46c2b19aa..5d8c25bae 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx @@ -35,8 +35,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox var buttonCharacterGirlfriend:Button; var buttonCharacterOpponent:Button; var inputBPM:NumberStepper; - var inputOffsetInst:NumberStepper; - var inputOffsetVocal:NumberStepper; var labelScrollSpeed:Label; var inputScrollSpeed:Slider; var frameVariation:Frame; @@ -138,25 +136,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox chartEditorState.updateTimeSignature(); }; - inputOffsetInst.onChange = function(event:UIEvent) { - if (event.value == null) return; - - chartEditorState.currentInstrumentalOffset = event.value; - Conductor.instance.instrumentalOffset = event.value; - // Update song length. - chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset; - }; - - inputOffsetVocal.onChange = function(event:UIEvent) { - if (event.value == null) return; - - chartEditorState.currentVocalOffset = event.value; - if (chartEditorState.audioVocalTrackGroup != null) - { - chartEditorState.audioVocalTrackGroup.playerVoicesOffset = event.value; - chartEditorState.audioVocalTrackGroup.opponentVoicesOffset = event.value; - } - }; inputScrollSpeed.onChange = function(event:UIEvent) { var valid:Bool = event.target.value != null && event.target.value > 0; @@ -196,8 +175,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox inputStage.value = chartEditorState.currentSongMetadata.playData.stage; inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle; inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm; - inputOffsetInst.value = chartEditorState.currentSongMetadata.offsets.getInstrumentalOffset(); - inputOffsetVocal.value = chartEditorState.currentSongMetadata.offsets.getVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player); inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed; labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x'; frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}'; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index 8dae1f7da..56b602758 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -1,42 +1,54 @@ package funkin.ui.debug.charting.toolboxes; -import funkin.play.character.BaseCharacter.CharacterType; -import funkin.play.character.CharacterData; -import funkin.data.stage.StageData; -import funkin.data.stage.StageRegistry; -import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; -import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import funkin.audio.SoundGroup; import haxe.ui.components.Button; -import haxe.ui.components.CheckBox; -import haxe.ui.components.DropDown; import haxe.ui.components.HorizontalSlider; import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; -import haxe.ui.components.TextField; -import funkin.play.stage.Stage; import funkin.ui.haxeui.components.WaveformPlayer; import funkin.audio.waveform.WaveformDataParser; -import haxe.ui.containers.Box; +import haxe.ui.containers.VBox; +import haxe.ui.containers.ScrollView; 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; -import funkin.audio.waveform.WaveformData; /** * 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/offsets.xml")) class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { + var waveformContainer:VBox; + var waveformScrollview:ScrollView; var waveformPlayer:WaveformPlayer; var waveformOpponent:WaveformPlayer; var waveformInstrumental:WaveformPlayer; var offsetButtonZoomIn:Button; var offsetButtonZoomOut:Button; + var offsetButtonPause:Button; + var offsetButtonPlay:Button; + var offsetButtonStop:Button; + var offsetStepperPlayer:NumberStepper; + var offsetStepperOpponent:NumberStepper; + var offsetStepperInstrumental:NumberStepper; - var waveformScale:Int = 64; + static final BASE_SCALE:Int = 64; + + var waveformScale:Int = BASE_SCALE; + + var audioPreviewTracks:SoundGroup; + + // Local store of the audio offsets, so we can detect when they change. + var audioPreviewPlayerOffset:Float = 0; + var audioPreviewOpponentOffset:Float = 0; + var audioPreviewInstrumentalOffset:Float = 0; public function new(chartEditorState2:ChartEditorState) { @@ -65,24 +77,223 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox offsetButtonZoomOut.onClick = (_) -> { zoomWaveformOut(); }; + offsetButtonPause.onClick = (_) -> { + pauseAudioPreview(); + }; + offsetButtonPlay.onClick = (_) -> { + playAudioPreview(); + }; + offsetButtonStop.onClick = (_) -> { + stopAudioPreview(); + }; + offsetStepperPlayer.onChange = (event:UIEvent) -> { + chartEditorState.currentVocalOffsetPlayer = event.value; + refresh(); + } + offsetStepperOpponent.onChange = (event:UIEvent) -> { + chartEditorState.currentVocalOffsetOpponent = event.value; + refresh(); + } + offsetStepperInstrumental.onChange = (event:UIEvent) -> { + chartEditorState.currentInstrumentalOffset = event.value; + refresh(); + } + waveformScrollview.onScroll = (_) -> { + if (!audioPreviewTracks.playing) + { + // We have to change the song position to match. + var currentWaveformIndex:Int = Std.int(waveformScrollview.hscrollPos / BASE_SCALE * waveformScale); + var targetSongTimeSeconds:Float = waveformPlayer.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(); + }; // Build player waveform. - waveformPlayer.waveform.forceUpdate = true; + // waveformPlayer.waveform.forceUpdate = true; waveformPlayer.waveform.waveformData = chartEditorState.audioVocalTrackGroup.buildPlayerVoiceWaveform(); // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it. - waveformPlayer.waveform.duration = 5.0; // chartEditorState.audioVocalTrackGroup.getPlayerVoiceLength() / 1000; + waveformPlayer.waveform.duration = chartEditorState.audioVocalTrackGroup.getPlayerVoiceLength() / Constants.MS_PER_SEC; // Build opponent waveform. - waveformOpponent.waveform.forceUpdate = true; + // waveformOpponent.waveform.forceUpdate = true; waveformOpponent.waveform.waveformData = chartEditorState.audioVocalTrackGroup.buildOpponentVoiceWaveform(); - waveformOpponent.waveform.duration = 5.0; // chartEditorState.audioVocalTrackGroup.getOpponentVoiceLength() / 1000; + waveformOpponent.waveform.duration = chartEditorState.audioVocalTrackGroup.getOpponentVoiceLength() / Constants.MS_PER_SEC; // Build instrumental waveform. - waveformInstrumental.waveform.forceUpdate = true; + // waveformInstrumental.waveform.forceUpdate = true; waveformInstrumental.waveform.waveformData = WaveformDataParser.interpretFlxSound(chartEditorState.audioInstTrack); - waveformInstrumental.waveform.duration = 5.0; // chartEditorState.audioInstTrack.length / 1000; + waveformInstrumental.waveform.duration = chartEditorState.audioInstTrack.length / Constants.MS_PER_SEC; refresh(); + refreshAudioPreview(); + + waveformPlayer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(PLAYER); + }); + waveformOpponent.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(OPPONENT); + }); + waveformInstrumental.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(INSTRUMENTAL); + }); + } + + /** + * 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(); + } + + audioPreviewTracks.add(chartEditorState.audioInstTrack.clone()); + audioPreviewTracks.add(chartEditorState.audioVocalTrackGroup.getPlayerVoice().clone()); + audioPreviewTracks.add(chartEditorState.audioVocalTrackGroup.getOpponentVoice().clone()); + + addOffsetsToAudioPreview(); + } + + var dragMousePosition:Float = 0; + var dragWaveform:Waveform = null; + + public function onStartDragWaveform(waveform:Waveform):Void + { + dragMousePosition = FlxG.mouse.x; + dragWaveform = waveform; + + Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragWaveform); + Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragWaveform); + } + + public function onDragWaveform(event:MouseEvent):Void + { + var newDragMousePosition = FlxG.mouse.x; + var deltaMousePosition = newDragMousePosition - dragMousePosition; + + if (deltaMousePosition == 0) return; + + var deltaPixels:Float = deltaMousePosition / BASE_SCALE * waveformScale; + var deltaMilliseconds:Float = switch (dragWaveform) + { + case PLAYER: + deltaPixels / waveformPlayer.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + case OPPONENT: + deltaPixels / waveformOpponent.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + case INSTRUMENTAL: + deltaPixels / waveformInstrumental.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + }; + + trace('Moving waveform by ${deltaMousePosition} -> ${deltaPixels} -> ${deltaMilliseconds} milliseconds.'); + + switch (dragWaveform) + { + case PLAYER: + chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds; + case OPPONENT: + chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds; + case INSTRUMENTAL: + chartEditorState.currentInstrumentalOffset += deltaMilliseconds; + } + + dragMousePosition = newDragMousePosition; + + refresh(); + } + + public function onStopDragWaveform(event:MouseEvent):Void + { + // Stop dragging. + Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragWaveform); + Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragWaveform); + + dragMousePosition = 0; + dragWaveform = null; + } + + public function playAudioPreview():Void + { + // chartEditorState.stopAudioPlayback(); + + audioPreviewTracks.resume(); + } + + public function addOffsetsToAudioPreview():Void + { + var trackInst = audioPreviewTracks.members[0]; + if (trackInst != null) + { + audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset; + trackInst.time -= audioPreviewInstrumentalOffset; + } + + var trackPlayer = audioPreviewTracks.members[1]; + if (trackPlayer != null) + { + audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer; + trackPlayer.time -= audioPreviewPlayerOffset; + } + + var trackOpponent = audioPreviewTracks.members[2]; + if (trackOpponent != null) + { + audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; + trackOpponent.time -= audioPreviewOpponentOffset; + } + } + + public function pauseAudioPreview():Void + { + audioPreviewTracks.pause(); + } + + public function stopAudioPreview():Void + { + audioPreviewTracks.stop(); + + audioPreviewTracks.time = 0; + + var trackInst = audioPreviewTracks.members[0]; + if (trackInst != null) + { + audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset; + trackInst.time = -audioPreviewInstrumentalOffset; + } + + var trackPlayer = audioPreviewTracks.members[1]; + if (trackPlayer != null) + { + audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer; + trackPlayer.time = -audioPreviewPlayerOffset; + } + + var trackOpponent = audioPreviewTracks.members[2]; + if (trackOpponent != null) + { + audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; + trackOpponent.time = -audioPreviewOpponentOffset; + } + + waveformScrollview.hscrollPos = 0; + refresh(); } public function zoomWaveformIn():Void @@ -96,6 +307,8 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox waveformScale = 1; } + trace('Zooming in, scale: ${waveformScale}'); + refresh(); } @@ -103,23 +316,84 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { waveformScale = Std.int(waveformScale * 2); + trace('Zooming out, scale: ${waveformScale}'); + refresh(); } + public override function update(elapsed:Float) + { + super.update(elapsed); + + if (audioPreviewTracks.playing) + { + trace('Playback time: ${audioPreviewTracks.time}'); + + var targetScrollPos:Float = waveformPlayer.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / waveformScale * BASE_SCALE; + waveformScrollview.hscrollPos = targetScrollPos; + } + + if (chartEditorState.currentInstrumentalOffset != audioPreviewInstrumentalOffset) + { + var track = audioPreviewTracks.members[0]; + if (track != null) + { + track.time += audioPreviewInstrumentalOffset; + track.time -= chartEditorState.currentInstrumentalOffset; + audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset; + } + } + if (chartEditorState.currentVocalOffsetPlayer != audioPreviewPlayerOffset) + { + var track = audioPreviewTracks.members[1]; + if (track != null) + { + track.time += audioPreviewPlayerOffset; + track.time -= chartEditorState.currentVocalOffsetPlayer; + audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer; + } + } + if (chartEditorState.currentVocalOffsetOpponent != audioPreviewOpponentOffset) + { + var track = audioPreviewTracks.members[2]; + if (track != null) + { + track.time += audioPreviewOpponentOffset; + track.time -= chartEditorState.currentVocalOffsetOpponent; + audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; + } + } + } + public override function refresh():Void { super.refresh(); // Set the width based on the waveformScale value. - waveformPlayer.waveform.width = waveformPlayer.waveform.waveformData.length / waveformScale; - trace('Player duration: ${waveformPlayer.waveform.duration}, width: ${waveformPlayer.waveform.width}'); + var maxWidth:Int = -1; - waveformOpponent.waveform.width = waveformOpponent.waveform.waveformData.length / waveformScale; - trace('Opponent duration: ${waveformOpponent.waveform.duration}, width: ${waveformOpponent.waveform.width}'); + offsetStepperPlayer.value = chartEditorState.currentVocalOffsetPlayer; + offsetStepperOpponent.value = chartEditorState.currentVocalOffsetOpponent; + offsetStepperInstrumental.value = chartEditorState.currentInstrumentalOffset; - waveformInstrumental.waveform.width = waveformInstrumental.waveform.waveformData.length / waveformScale; - trace('Instrumental duration: ${waveformInstrumental.waveform.duration}, width: ${waveformInstrumental.waveform.width}'); + waveformPlayer.waveform.time = -chartEditorState.currentVocalOffsetPlayer / Constants.MS_PER_SEC; // Negative offsets make the song start early. + waveformPlayer.waveform.width = waveformPlayer.waveform.waveformData.length / waveformScale * BASE_SCALE; + if (waveformPlayer.waveform.width > maxWidth) maxWidth = Std.int(waveformPlayer.waveform.width); + + waveformOpponent.waveform.time = -chartEditorState.currentVocalOffsetOpponent / Constants.MS_PER_SEC; + waveformOpponent.waveform.width = waveformOpponent.waveform.waveformData.length / waveformScale * BASE_SCALE; + if (waveformOpponent.waveform.width > maxWidth) maxWidth = Std.int(waveformOpponent.waveform.width); + + waveformInstrumental.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC; + waveformInstrumental.waveform.width = waveformInstrumental.waveform.waveformData.length / waveformScale * BASE_SCALE; + if (waveformInstrumental.waveform.width > maxWidth) maxWidth = Std.int(waveformInstrumental.waveform.width); + + waveformPlayer.waveform.markDirty(); + waveformOpponent.waveform.markDirty(); + waveformInstrumental.waveform.markDirty(); + + waveformContainer.width = maxWidth; } public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox @@ -127,3 +401,10 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox return new ChartEditorOffsetsToolbox(chartEditorState); } } + +enum Waveform +{ + PLAYER; + OPPONENT; + INSTRUMENTAL; +} diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index 82db68d30..ad5983e52 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -199,6 +199,24 @@ class CrashHandler throw "This is an example of an uncaught exception."; } + public static function induceNullObjectReference():Void + { + var obj:Dynamic = null; + var value = obj.test; + } + + public static function induceNullObjectReference2():Void + { + var obj:Dynamic = null; + var value = obj.test(); + } + + public static function induceNullObjectReference3():Void + { + var obj:Dynamic = null; + var value = obj(); + } + static function renderMethod():String { return switch (FlxG.renderMethod)