mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-27 10:05:41 -05:00
847 lines
29 KiB
Haxe
847 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;
|
|
}
|