Finalize freeplay preview toolbox

This commit is contained in:
EliteMasterEric 2024-02-09 14:58:57 -05:00
parent 401c200070
commit 4bf9f68658
17 changed files with 1067 additions and 74 deletions

2
assets

@ -1 +1 @@
Subproject commit 251d4640bd77ee0f0b6122a13f123274c43dd3f5
Subproject commit 1941ec605d2da5a27e41588515d13d5bfc882582

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}
/**

View File

@ -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.
*/

View File

@ -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;
}

View File

@ -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}';

View File

@ -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";

View File

@ -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.
*/

View File

@ -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

View File

@ -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";
}
}
}

View File

@ -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;

View File

@ -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');
}
/**

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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));

View File

@ -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> = [];