1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-09-20 17:18:55 +00:00
Funkin/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
2024-03-06 05:06:57 +00:00

848 lines
29 KiB
Haxe

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.SetAudioOffsetCommand;
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 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/offsets.xml"))
class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
{
var waveformContainer:Absolute;
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 offsetTicksContainer:Absolute;
var playheadSprite: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 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 = BASE_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;
}
/**
* 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 tickLabels:Array<Label> = [];
// 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)
{
super(chartEditorState2);
initialize();
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
chartEditorState.menubarItemToggleToolboxOffsets.selected = false;
}
function initialize():Void
{
// Starting position.
// TODO: Save and load this.
this.x = 150;
this.y = 250;
offsetPlayerVolume.onChange = (_) -> {
var targetVolume = offsetPlayerVolume.value * 2 / 100;
setTrackVolume(PLAYER, targetVolume);
};
offsetPlayerMute.onClick = (_) -> {
toggleMuteTrack(PLAYER);
};
offsetPlayerSolo.onClick = (_) -> {
soloTrack(PLAYER);
};
offsetOpponentVolume.onChange = (_) -> {
var targetVolume = offsetOpponentVolume.value * 2 / 100;
setTrackVolume(OPPONENT, targetVolume);
};
offsetOpponentMute.onClick = (_) -> {
toggleMuteTrack(OPPONENT);
};
offsetOpponentSolo.onClick = (_) -> {
soloTrack(OPPONENT);
};
offsetInstrumentalVolume.onChange = (_) -> {
var targetVolume = offsetInstrumentalVolume.value * 2 / 100;
setTrackVolume(INSTRUMENTAL, targetVolume);
};
offsetInstrumentalMute.onClick = (_) -> {
toggleMuteTrack(INSTRUMENTAL);
};
offsetInstrumentalSolo.onClick = (_) -> {
soloTrack(INSTRUMENTAL);
};
offsetButtonZoomIn.onClick = (_) -> {
zoomWaveformIn();
};
offsetButtonZoomOut.onClick = (_) -> {
zoomWaveformOut();
};
offsetButtonPause.onClick = (_) -> {
pauseAudioPreview();
};
offsetButtonPlay.onClick = (_) -> {
playAudioPreview();
};
offsetButtonStop.onClick = (_) -> {
stopAudioPreview();
};
offsetStepperPlayer.onChange = (event:UIEvent) -> {
if (event.value == chartEditorState.currentVocalOffsetPlayer) return;
if (dragWaveform != null) return;
chartEditorState.performCommand(new SetAudioOffsetCommand(PLAYER, event.value));
refresh();
}
offsetStepperOpponent.onChange = (event:UIEvent) -> {
if (event.value == chartEditorState.currentVocalOffsetOpponent) return;
if (dragWaveform != null) return;
chartEditorState.performCommand(new SetAudioOffsetCommand(OPPONENT, event.value));
refresh();
}
offsetStepperInstrumental.onChange = (event:UIEvent) -> {
if (event.value == chartEditorState.currentInstrumentalOffset) return;
if (dragWaveform != null) return;
chartEditorState.performCommand(new SetAudioOffsetCommand(INSTRUMENTAL, 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);
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 = 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();
};
initializeTicks();
refreshAudioPreview();
refresh();
refreshTicks();
waveformPlayer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
onStartDragWaveform(PLAYER);
});
waveformOpponent.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
onStartDragWaveform(OPPONENT);
});
waveformInstrumental.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
onStartDragWaveform(INSTRUMENTAL);
});
offsetTicksContainer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
onStartDragPlayhead();
});
}
function initializeTicks():Void
{
tickTiledSprite = new FlxTiledSprite(chartEditorState.offsetTickBitmap, 100, chartEditorState.offsetTickBitmap.height, true, false);
offsetTicksSprite.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.
// waveformPlayer.waveform.forceUpdate = true;
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 ?? 1000.0) / Constants.MS_PER_SEC;
// Build opponent waveform.
// waveformOpponent.waveform.forceUpdate = true;
// note: if song only has one set of vocals (Vocals.ogg/mp3) then this is null and crashes charting editor
// so we null check
waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
// Build instrumental waveform.
// waveformInstrumental.waveform.forceUpdate = true;
waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000.0) / Constants.MS_PER_SEC;
addOffsetsToAudioPreview();
}
public function refreshTicks():Void
{
while (tickLabels.length > 0)
{
var label = tickLabels.pop();
offsetTicksContainer.removeComponent(label);
}
var labelYPos:Float = chartEditorState.offsetTickBitmap.height / 2;
var labelHeight:Float = chartEditorState.offsetTickBitmap.height / 2;
var numberOfTicks:Int = Math.floor(waveformInstrumental.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) / waveformInstrumental.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;
offsetTicksContainer.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 = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex);
audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
}
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);
}
var dragMousePosition:Float = 0;
var dragWaveform:Waveform = null;
var dragOffsetMs:Float = 0;
public function onDragWaveform(event:MouseEvent):Void
{
var newDragMousePosition = FlxG.mouse.x;
var deltaMousePosition = newDragMousePosition - dragMousePosition;
if (deltaMousePosition == 0) return;
var deltaPixels:Float = deltaMousePosition * (waveformScale / BASE_SCALE * waveformMagicFactor);
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;
};
switch (dragWaveform)
{
case PLAYER:
// chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds;
dragOffsetMs += deltaMilliseconds;
offsetStepperPlayer.value += deltaMilliseconds;
case OPPONENT:
// chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds;
dragOffsetMs += deltaMilliseconds;
offsetStepperOpponent.value += deltaMilliseconds;
case INSTRUMENTAL:
// chartEditorState.currentInstrumentalOffset += deltaMilliseconds;
dragOffsetMs += deltaMilliseconds;
offsetStepperInstrumental.value += 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);
// Apply the offset change after dragging happens.
// We only do this once per drag so we don't get 20 commands a second in the history.
if (dragOffsetMs != 0)
{
// false to not refresh this toolbox, we will manually do that later.
switch (dragWaveform)
{
case PLAYER:
chartEditorState.performCommand(new SetAudioOffsetCommand(PLAYER, chartEditorState.currentVocalOffsetPlayer + dragOffsetMs, false));
case OPPONENT:
chartEditorState.performCommand(new SetAudioOffsetCommand(OPPONENT, chartEditorState.currentVocalOffsetOpponent + dragOffsetMs, false));
case INSTRUMENTAL:
chartEditorState.performCommand(new SetAudioOffsetCommand(INSTRUMENTAL, chartEditorState.currentInstrumentalOffset + dragOffsetMs, false));
}
}
dragOffsetMs = 0;
dragMousePosition = 0;
dragWaveform = null;
refresh();
addOffsetsToAudioPreview();
}
public function playAudioPreview():Void
{
audioPreviewTracks.play(false, audioPreviewTracks.time);
}
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;
playheadAbsolutePos = 0 + playheadSprite.width;
refresh();
addOffsetsToAudioPreview();
}
public function zoomWaveformIn():Void
{
if (waveformScale > MIN_SCALE)
{
waveformScale = waveformScale / WAVEFORM_ZOOM_MULT;
if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;
// 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;
// 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(target:Waveform, volume:Float):Void
{
switch (target)
{
case Waveform.INSTRUMENTAL:
var trackInst = audioPreviewTracks.members[0];
if (trackInst != null)
{
trackInst.volume = volume;
}
case Waveform.PLAYER:
var trackPlayer = audioPreviewTracks.members[1];
if (trackPlayer != null)
{
trackPlayer.volume = volume;
}
case Waveform.OPPONENT:
var trackOpponent = audioPreviewTracks.members[2];
if (trackOpponent != null)
{
trackOpponent.volume = volume;
}
}
}
public function muteTrack(target:Waveform):Void
{
switch (target)
{
case Waveform.INSTRUMENTAL:
var trackInst = audioPreviewTracks.members[0];
if (trackInst != null)
{
trackInst.muted = true;
offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
}
case Waveform.PLAYER:
var trackPlayer = audioPreviewTracks.members[1];
if (trackPlayer != null)
{
trackPlayer.muted = true;
offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
}
case Waveform.OPPONENT:
var trackOpponent = audioPreviewTracks.members[2];
if (trackOpponent != null)
{
trackOpponent.muted = true;
offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
}
}
}
public function unmuteTrack(target:Waveform):Void
{
switch (target)
{
case Waveform.INSTRUMENTAL:
var trackInst = audioPreviewTracks.members[0];
if (trackInst != null)
{
trackInst.muted = false;
offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
}
case Waveform.PLAYER:
var trackPlayer = audioPreviewTracks.members[1];
if (trackPlayer != null)
{
trackPlayer.muted = false;
offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
}
case Waveform.OPPONENT:
var trackOpponent = audioPreviewTracks.members[2];
if (trackOpponent != null)
{
trackOpponent.muted = false;
offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
}
}
}
public function toggleMuteTrack(target:Waveform):Void
{
switch (target)
{
case Waveform.INSTRUMENTAL:
var trackInst = audioPreviewTracks.members[0];
if (trackInst != null)
{
trackInst.muted = !trackInst.muted;
offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
}
case Waveform.PLAYER:
var trackPlayer = audioPreviewTracks.members[1];
if (trackPlayer != null)
{
trackPlayer.muted = !trackPlayer.muted;
offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
}
case Waveform.OPPONENT:
var trackOpponent = audioPreviewTracks.members[2];
if (trackOpponent != null)
{
trackOpponent.muted = !trackOpponent.muted;
offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
}
}
}
/**
* Clicking the solo button will unmute the track and mute all other tracks.
* @param target
*/
public function soloTrack(target:Waveform):Void
{
switch (target)
{
case Waveform.PLAYER:
muteTrack(Waveform.OPPONENT);
muteTrack(Waveform.INSTRUMENTAL);
unmuteTrack(Waveform.PLAYER);
case Waveform.OPPONENT:
muteTrack(Waveform.PLAYER);
muteTrack(Waveform.INSTRUMENTAL);
unmuteTrack(Waveform.OPPONENT);
case Waveform.INSTRUMENTAL:
muteTrack(Waveform.PLAYER);
muteTrack(Waveform.OPPONENT);
unmuteTrack(Waveform.INSTRUMENTAL);
}
}
public override function update(elapsed:Float)
{
super.update(elapsed);
if (audioPreviewTracks.playing)
{
trace('Playback time: ${audioPreviewTracks.time}');
var targetScrollPos:Float = waveformInstrumental.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 A.
// Just move the scroll view with the playhead, constraining it so that the playhead is always visible.
// waveformScrollview.hscrollPos += playheadDiff;
// waveformScrollview.hscrollPos = FlxMath.bound(waveformScrollview.hscrollPos, playheadAbsolutePos - playheadSprite.width, playheadAbsolutePos);
// BEHAVIOR B.
// Keep `playheadAbsolutePos` within the bounds of the screen.
// The scroll view will eventually move to where the playhead is 1/8th of the way from the left. This looks kinda nice!
// TODO: This causes a hard snap to scroll when the playhead is to the right of the playheadCenterPoint.
// var playheadCenterPoint = waveformScrollview.width / 8;
// waveformScrollview.hscrollPos = FlxMath.bound(waveformScrollview.hscrollPos, playheadAbsolutePos - playheadCenterPoint, playheadAbsolutePos);
// playheadRelativePos = 0;
// 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;
}
}
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;
}
}
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));
}
public override function refresh():Void
{
super.refresh();
waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformInstrumental.waveform.waveformData.pointsPerSecond());
var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor;
var maxWidth:Int = -1;
offsetStepperPlayer.value = chartEditorState.currentVocalOffsetPlayer;
offsetStepperOpponent.value = chartEditorState.currentVocalOffsetOpponent;
offsetStepperInstrumental.value = chartEditorState.currentInstrumentalOffset;
waveformPlayer.waveform.time = -chartEditorState.currentVocalOffsetPlayer / Constants.MS_PER_SEC; // Negative offsets make the song start early.
waveformPlayer.waveform.width = (waveformPlayer.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
if (waveformPlayer.waveform.width > maxWidth) maxWidth = Std.int(waveformPlayer.waveform.width);
waveformPlayer.waveform.height = 65;
waveformOpponent.waveform.time = -chartEditorState.currentVocalOffsetOpponent / Constants.MS_PER_SEC;
waveformOpponent.waveform.width = (waveformOpponent.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
if (waveformOpponent.waveform.width > maxWidth) maxWidth = Std.int(waveformOpponent.waveform.width);
waveformOpponent.waveform.height = 65;
waveformInstrumental.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC;
waveformInstrumental.waveform.width = (waveformInstrumental.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
if (waveformInstrumental.waveform.width > maxWidth) maxWidth = Std.int(waveformInstrumental.waveform.width);
waveformInstrumental.waveform.height = 65;
// Live update the drag, but don't actually change the underlying offset until we release the mouse to finish dragging.
if (dragWaveform != null) switch (dragWaveform)
{
case PLAYER:
// chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds;
waveformPlayer.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
offsetStepperPlayer.value += dragOffsetMs;
case OPPONENT:
// chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds;
waveformOpponent.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
offsetStepperOpponent.value += dragOffsetMs;
case INSTRUMENTAL:
// chartEditorState.currentInstrumentalOffset += deltaMilliseconds;
waveformInstrumental.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
offsetStepperInstrumental.value += dragOffsetMs;
default:
// No drag, no
}
waveformPlayer.waveform.markDirty();
waveformOpponent.waveform.markDirty();
waveformInstrumental.waveform.markDirty();
waveformContainer.width = maxWidth;
tickTiledSprite.width = maxWidth;
}
public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox
{
return new ChartEditorOffsetsToolbox(chartEditorState);
}
}
enum Waveform
{
PLAYER;
OPPONENT;
INSTRUMENTAL;
}