Note quant snapping

This commit is contained in:
EliteMasterEric 2022-12-14 15:33:30 -05:00
parent 234dc0ac19
commit a2d803cc83
10 changed files with 633 additions and 147 deletions

View file

@ -11,14 +11,14 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "8ff2aa9",
"ref": "a629f9a5",
"url": "https://github.com/MasterEric/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "157eaf3",
"ref": "752c3d7",
"url": "https://github.com/MasterEric/flixel-addons"
},
{
@ -30,26 +30,26 @@
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "58d5d27",
"ref": "18b2060",
"url": "https://github.com/Dot-Stuff/flxanimate"
},
{
"name": "format",
"type": "haxelib",
"version": null
"version": "3.5.0"
},
{
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "3f229bd",
"ref": "fc8d656b",
"url": "https://github.com/haxeui/haxeui-core/"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "be1f404",
"ref": "80941a7",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@ -57,11 +57,6 @@
"type": "haxelib",
"version": "2.1.0"
},
{
"name": "haxe-concurrent",
"type": "haxelib",
"version": "3.0.2"
},
{
"name": "hscript",
"type": "haxelib",
@ -84,23 +79,21 @@
},
{
"name": "lime",
"type": "git",
"dir": null,
"ref": "4c82152",
"url": "https://github.com/openfl/lime"
"type": "haxelib",
"version": null
},
{
"name": "openfl",
"type": "git",
"dir": null,
"ref": "3fd5763",
"url": "https://github.com/MasterEric/openfl/"
"ref": "3fd5763c1",
"url": "https://github.com/MasterEric/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "develop",
"ref": "547c8ee",
"url": "https://github.com/larsiusprime/polymod"
},
{

View file

@ -63,13 +63,33 @@ class Conductor
}
/**
* Duration of a step in milliseconds. Calculated based on bpm.
* Duration of a step (quarter) in milliseconds. Calculated based on bpm.
*/
public static var stepCrochet(get, null):Float;
static function get_stepCrochet():Float
{
return crochet / 4;
return crochet / timeSignatureNumerator;
}
public static var timeSignatureNumerator(get, null):Int;
static function get_timeSignatureNumerator():Int
{
if (currentTimeChange == null)
return 4;
return currentTimeChange.timeSignatureNum;
}
public static var timeSignatureDenominator(get, null):Int;
static function get_timeSignatureDenominator():Int
{
if (currentTimeChange == null)
return 4;
return currentTimeChange.timeSignatureDen;
}
/**
@ -124,10 +144,12 @@ class Conductor
* Forcibly defines the current BPM of the song.
* Useful for things like the chart editor that need to manipulate BPM in real time.
*
* Set to null to reset to the BPM defined by the timeChanges.
*
* WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead.
*/
public static function forceBPM(bpm:Float)
public static function forceBPM(?bpm:Float = null)
{
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
Conductor.bpmOverride = bpm;

View file

@ -77,6 +77,7 @@ class MusicBeatState extends FlxUIState
}
FlxG.watch.addQuick("songPos", Conductor.songPosition);
FlxG.watch.addQuick("bpm", Conductor.bpm);
dispatchEvent(new UpdateScriptEvent(elapsed));
}

View file

@ -114,7 +114,7 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
var result:FlxSound = super.add(sound);
if (result == null)
return;
return null;
// Apply parameters to the new sound.
result.autoDestroy = this.autoDestroyMembers;
@ -126,6 +126,8 @@ class FlxAudioGroup extends FlxTypedGroup<FlxSound>
result.play(true, 0.0);
result.pause();
result.time = this.time;
return result;
}
/**

View file

@ -25,10 +25,12 @@ class PolymodHandler
public static function createModRoot()
{
#if sys
if (!sys.FileSystem.exists(MOD_FOLDER))
{
sys.FileSystem.createDirectory(MOD_FOLDER);
}
#end
}
/**

View file

@ -196,11 +196,11 @@ class HealthIcon extends FlxSprite
// Make the health icons bump (the update function causes them to lerp back down).
if (this.width > this.height)
{
setGraphicSize(this.width + (HEALTH_ICON_SIZE * this.size.x * 0.2), 0);
setGraphicSize(Std.int(this.width + (HEALTH_ICON_SIZE * this.size.x * 0.2)), 0);
}
else
{
setGraphicSize(0, this.height + (HEALTH_ICON_SIZE * this.size.y * 0.2));
setGraphicSize(0, Std.int(this.height + (HEALTH_ICON_SIZE * this.size.y * 0.2)));
}
this.updateHitbox();
}

View file

@ -1,5 +1,7 @@
package funkin.ui.debug.charting;
import flixel.FlxObject;
import flixel.FlxBasic;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
@ -12,17 +14,14 @@ import funkin.play.song.SongData.SongNoteData;
*/
class ChartEditorNoteSprite extends FlxSprite
{
public var parentState:ChartEditorState;
/**
* The note data that this sprite represents.
* You can set this to null to kill the sprite and flag it for recycling.
*/
public var noteData(default, set):SongNoteData;
/**
* The note skin that this sprite displays.
*/
public var noteSkin(default, set):String = 'Normal';
/**
* This note is the previous sprite in a sustain chain.
*/
@ -33,10 +32,12 @@ class ChartEditorNoteSprite extends FlxSprite
*/
public var childNoteSprite(default, set):ChartEditorNoteSprite = null;
public function new()
public function new(parent:ChartEditorState)
{
super();
this.parentState = parent;
if (noteFrameCollection == null)
{
initFrameCollection();
@ -131,26 +132,12 @@ class ChartEditorNoteSprite extends FlxSprite
playNoteAnimation();
// Update the position to match the note data.
setNotePosition();
updateNotePosition();
return this.noteData;
}
function set_noteSkin(value:String):String
{
// Don't update if the skin hasn't changed.
if (value == this.noteSkin)
return this.noteSkin;
this.noteSkin = value;
// Make sure to update the graphic to match the note skin.
playNoteAnimation();
return this.noteSkin;
}
function setNotePosition()
public function updateNotePosition(?origin:FlxObject)
{
var cursorColumn:Int = this.noteData.data;
@ -179,7 +166,13 @@ class ChartEditorNoteSprite extends FlxSprite
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
// TODO: stepTime doesn't account for fluctuating BPMs.
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
if (this.noteData.stepTime >= 0)
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) {
this.x += origin.x;
this.y += origin.y;
}
}
else
{
@ -214,7 +207,6 @@ class ChartEditorNoteSprite extends FlxSprite
if (this.parentNoteSprite != null)
{
this.noteData = this.parentNoteSprite.noteData;
this.noteSkin = this.parentNoteSprite.noteSkin;
}
return this.parentNoteSprite;
@ -227,13 +219,12 @@ class ChartEditorNoteSprite extends FlxSprite
if (this.parentNoteSprite != null)
{
this.noteData = this.parentNoteSprite.noteData;
this.noteSkin = this.parentNoteSprite.noteSkin;
}
return this.childNoteSprite;
}
function playNoteAnimation()
public function playNoteAnimation()
{
// Decide whether to display a note or a sustain.
var baseAnimationName:String = 'tap';
@ -241,7 +232,7 @@ class ChartEditorNoteSprite extends FlxSprite
baseAnimationName = (this.childNoteSprite != null) ? 'hold' : 'holdEnd';
// Play the appropriate animation for the type, direction, and skin.
var animationName = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteSkin}';
var animationName = '${baseAnimationName}${this.noteData.getDirectionName()}${this.parentState.currentSongNoteSkin}';
this.animation.play(animationName);
@ -266,7 +257,7 @@ class ChartEditorNoteSprite extends FlxSprite
this.updateHitbox();
// TODO: Make this an attribute of the note skin.
this.antialiasing = (noteSkin != 'Pixel');
this.antialiasing = (this.parentState.currentSongNoteSkin != 'Pixel');
}
/**

View file

@ -117,16 +117,31 @@ class ChartEditorState extends HaxeUIState
*/
static final DRAG_THRESHOLD:Float = 16.0;
/**
* Types of notes you can snap to.
*/
static final SNAP_QUANTS:Array<Int> = [4, 8, 12, 16, 20, 24, 32, 48, 64, 96, 192];
/**
* INSTANCE DATA
*/
// ==============================
public var currentZoomLevel:Float = 1.0;
var noteSnapQuantIndex:Int = 3;
public var noteSnapQuant(get, never):Int;
function get_noteSnapQuant():Int
{
return SNAP_QUANTS[noteSnapQuantIndex];
}
/**
* scrollPosition is the current position in the song, in pixels.
* One pixel is 1/40 of 1 step, and 1/160 of 1 beat.
*/
var scrollPosition(default, set):Float = -1.0;
var scrollPositionInPixels(default, set):Float = -1.0;
/**
* scrollPosition, converted to steps.
@ -136,7 +151,7 @@ class ChartEditorState extends HaxeUIState
function get_scrollPositionInSteps():Float
{
return scrollPosition / GRID_SIZE;
return scrollPositionInPixels / GRID_SIZE;
}
/**
@ -152,7 +167,7 @@ class ChartEditorState extends HaxeUIState
function set_scrollPositionInMs(value:Float):Float
{
scrollPosition = value / Conductor.stepCrochet;
scrollPositionInPixels = value / Conductor.stepCrochet;
return value;
}
@ -162,7 +177,7 @@ class ChartEditorState extends HaxeUIState
* 40 means the playhead is 1 grid length below the base position.
* -40 means the playhead is 1 grid length above the base position.
*/
var playheadPosition(default, set):Float;
var playheadPositionInPixels(default, set):Float;
var playheadPositionInSteps(get, null):Float;
@ -171,7 +186,7 @@ class ChartEditorState extends HaxeUIState
*/
function get_playheadPositionInSteps():Float
{
return playheadPosition / GRID_SIZE;
return playheadPositionInPixels / GRID_SIZE;
}
/**
@ -187,14 +202,14 @@ class ChartEditorState extends HaxeUIState
/**
* This is the song's length in PIXELS, same format as scrollPosition.
*/
var songLength(get, default):Int;
var songLengthInPixels(get, default):Int;
function get_songLength():Int
function get_songLengthInPixels():Int
{
if (songLength <= 0)
if (songLengthInPixels <= 0)
return 1000;
return songLength;
return songLengthInPixels;
}
/**
@ -204,7 +219,7 @@ class ChartEditorState extends HaxeUIState
function get_songLengthInSteps():Float
{
return songLength / GRID_SIZE;
return songLengthInPixels / GRID_SIZE;
}
/**
@ -273,7 +288,7 @@ class ChartEditorState extends HaxeUIState
// Make sure view is updated when we change view modes.
noteDisplayDirty = true;
notePreviewDirty = true;
this.scrollPosition = this.scrollPosition;
this.scrollPositionInPixels = this.scrollPositionInPixels;
return isViewDownscroll;
}
@ -367,7 +382,7 @@ class ChartEditorState extends HaxeUIState
// Make sure view is updated when we change modes.
noteDisplayDirty = true;
notePreviewDirty = true;
this.scrollPosition = 0;
this.scrollPositionInPixels = 0;
return isInPatternMode;
}
@ -595,7 +610,7 @@ class ChartEditorState extends HaxeUIState
return value;
}
var currentSongNoteSkin(get, set):String;
public var currentSongNoteSkin(get, set):String;
function get_currentSongNoteSkin():String
{
@ -696,9 +711,9 @@ class ChartEditorState extends HaxeUIState
var gridPlayheadScrollArea:FlxSprite;
/**
* A sprite used to highlight the grid square under the cursor.
* A sprite used to indicate the note that will be placed on click.
*/
var gridCursor:FlxSprite;
var gridGhostNote:ChartEditorNoteSprite;
/**
* The waveform which (optionally) displays over the grid, underneath the notes and playhead.
@ -823,14 +838,11 @@ class ChartEditorState extends HaxeUIState
gridTiledSprite.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; // Push down to account for the menu bar.
add(gridTiledSprite);
/*
buildSpectrogram(audioVocalTrack);
*/
// The cursor that appears when hovering over the grid.
var gridCursorSize:Int = Std.int(GRID_SIZE - GRID_SELECTION_BORDER_WIDTH);
gridCursor = new FlxSprite().makeGraphic(gridCursorSize, gridCursorSize, CURSOR_COLOR);
add(gridCursor);
gridGhostNote = new ChartEditorNoteSprite(this);
gridGhostNote.alpha = 0.8;
gridGhostNote.noteData = new SongNoteData(-1, -1, 0, "");
gridGhostNote.visible = false;
add(gridGhostNote);
buildNoteGroup();
@ -975,7 +987,7 @@ class ChartEditorState extends HaxeUIState
playbarHeadDragging = false;
// Set the song position to where the playhead was moved to.
scrollPosition = songLength * (playbarHead.value / 100);
scrollPositionInPixels = songLengthInPixels * (playbarHead.value / 100);
// Update the conductor and audio tracks to match.
moveSongToScrollPosition();
@ -1088,13 +1100,13 @@ class ChartEditorState extends HaxeUIState
hitsoundsEnabledPlayer = event.value;
});
setUISelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer);
addUIChangeListener('menubarItemOpponentHitsounds', (event:UIEvent) ->
{
hitsoundsEnabledOpponent = event.value;
});
setUISelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label);
addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) ->
{
@ -1210,6 +1222,8 @@ class ChartEditorState extends HaxeUIState
// These ones only happen if the modal dialog is not open.
handleScrollKeybinds();
handleZoom();
handleSnap();
handleCursor();
handleMenubar();
@ -1326,7 +1340,7 @@ class ChartEditorState extends HaxeUIState
}
// Mouse Wheel = Scroll
if (FlxG.mouse.wheel != 0)
if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -10 * FlxG.mouse.wheel;
}
@ -1365,37 +1379,73 @@ class ChartEditorState extends HaxeUIState
if (FlxG.keys.justPressed.HOME)
{
// Scroll amount is the difference between the current position and the top.
scrollAmount = 0 - this.scrollPosition;
playheadAmount = 0 - this.playheadPosition;
scrollAmount = 0 - this.scrollPositionInPixels;
playheadAmount = 0 - this.playheadPositionInPixels;
}
if (playbarButtonPressed == 'playbarStart')
{
playbarButtonPressed = '';
scrollAmount = 0 - this.scrollPosition;
playheadAmount = 0 - this.playheadPosition;
scrollAmount = 0 - this.scrollPositionInPixels;
playheadAmount = 0 - this.playheadPositionInPixels;
}
// END = Scroll to Bottom
if (FlxG.keys.justPressed.END)
{
// Scroll amount is the difference between the current position and the bottom.
scrollAmount = this.songLength - this.scrollPosition;
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
}
if (playbarButtonPressed == 'playbarEnd')
{
playbarButtonPressed = '';
scrollAmount = this.songLength - this.scrollPosition;
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
}
// Apply the scroll amount.
this.scrollPosition += scrollAmount;
this.playheadPosition += playheadAmount;
this.scrollPositionInPixels += scrollAmount;
this.playheadPositionInPixels += playheadAmount;
// Resync the conductor and audio tracks.
if (scrollAmount != 0 || playheadAmount != 0)
moveSongToScrollPosition();
}
function handleZoom()
{
if (FlxG.keys.justPressed.MINUS)
{
currentZoomLevel /= 2;
// Update the grid.
ChartEditorThemeHandler.updateTheme(this);
// Update the note positions.
noteDisplayDirty = true;
}
if (FlxG.keys.justPressed.PLUS)
{
currentZoomLevel *= 2;
// Update the grid.
ChartEditorThemeHandler.updateTheme(this);
// Update the note positions.
noteDisplayDirty = true;
}
}
function handleSnap()
{
if (FlxG.keys.justPressed.LEFT)
{
noteSnapQuantIndex--;
}
if (FlxG.keys.justPressed.RIGHT)
{
noteSnapQuantIndex++;
}
}
/**
* Handle display of the mouse cursor.
*/
@ -1403,6 +1453,7 @@ class ChartEditorState extends HaxeUIState
{
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
var shouldHandleCursor = !isCursorOverHaxeUI || (selectionBoxStartPos != null);
var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1;
if (shouldHandleCursor)
{
@ -1451,16 +1502,16 @@ class ChartEditorState extends HaxeUIState
{
// Clicked on the playhead scroll area.
// Move the playhead to the cursor position.
this.playheadPosition = FlxG.mouse.screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD;
this.playheadPositionInPixels = FlxG.mouse.screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD;
moveSongToScrollPosition();
}
// Cursor position snapped to the grid.
// The song position of the cursor, in steps.
var cursorFractionalStep:Float = cursorY / GRID_SIZE;
var cursorStep:Int = Math.floor(cursorFractionalStep);
var cursorMs:Float = cursorStep * Conductor.stepCrochet;
var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant);
var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep));
var cursorMs:Float = cursorStep * Conductor.stepCrochet * (16 / noteSnapQuant);
// The direction value for the column at the cursor.
var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE);
if (cursorColumn < 0)
@ -1702,7 +1753,7 @@ class ChartEditorState extends HaxeUIState
else
{
// Click a blank space to place a note and select it.
var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1;
if (cursorColumn == eventColumn)
{
// Create an event and place it in the chart.
@ -1750,28 +1801,44 @@ class ChartEditorState extends HaxeUIState
performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
}
}
}
// Handle grid cursor.
if (overlapsGrid && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed)
{
gridCursor.visible = true;
// X and Y are the cursor position relative to the grid, snapped to the top left of the grid square.
gridCursor.x = Math.floor(cursorX / GRID_SIZE) * GRID_SIZE + gridTiledSprite.x + (GRID_SELECTION_BORDER_WIDTH / 2);
gridCursor.y = cursorStep * GRID_SIZE + gridTiledSprite.y + (GRID_SELECTION_BORDER_WIDTH / 2);
}
else
{
gridCursor.visible = false;
gridCursor.x = -9999;
gridCursor.y = -9999;
// Handle grid cursor.
if (overlapsGrid && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed)
{
Cursor.cursorMode = Pointer;
// Indicate that we can pla
gridGhostNote.visible = (cursorColumn != eventColumn);
if (cursorColumn != gridGhostNote.noteData.data || selectedNoteKind != gridGhostNote.noteData.kind) {
gridGhostNote.noteData.kind = selectedNoteKind;
gridGhostNote.noteData.data = cursorColumn;
gridGhostNote.playNoteAnimation();
}
FlxG.watch.addQuick("cursorY", cursorY);
FlxG.watch.addQuick("cursorFractionalStep", cursorFractionalStep);
FlxG.watch.addQuick("cursorStep", cursorStep);
FlxG.watch.addQuick("cursorMs", cursorMs);
gridGhostNote.noteData.time = cursorMs;
gridGhostNote.updateNotePosition(renderedNotes);
// gridCursor.visible = true;
// // X and Y are the cursor position relative to the grid, snapped to the top left of the grid square.
// gridCursor.x = Math.floor(cursorX / GRID_SIZE) * GRID_SIZE + gridTiledSprite.x + (GRID_SELECTION_BORDER_WIDTH / 2);
// gridCursor.y = cursorStep * GRID_SIZE + gridTiledSprite.y + (GRID_SELECTION_BORDER_WIDTH / 2);
}
else
{
gridGhostNote.visible = false;
Cursor.cursorMode = Default;
}
}
}
else
{
gridCursor.visible = false;
gridCursor.x = -9999;
gridCursor.y = -9999;
gridGhostNote.visible = false;
}
if (isCursorOverHaxeUIButton && Cursor.cursorMode == Default)
@ -1793,9 +1860,9 @@ class ChartEditorState extends HaxeUIState
renderedNotes.flipX = (isViewDownscroll);
// Calculate the view bounds.
var viewAreaTop:Float = this.scrollPosition - GRID_TOP_PAD;
var viewAreaTop:Float = this.scrollPositionInPixels - GRID_TOP_PAD;
var viewHeight:Float = (FlxG.height - MENU_BAR_HEIGHT);
var viewAreaBottom:Float = this.scrollPosition + viewHeight;
var viewAreaBottom:Float = this.scrollPositionInPixels + viewHeight;
// Remove notes that are no longer visible and list the ones that are.
var displayedNoteData:Array<SongNoteData> = [];
@ -1830,7 +1897,11 @@ class ChartEditorState extends HaxeUIState
}
else
{
// Note is already displayed and should remain displayed.
displayedNoteData.push(noteSprite.noteData);
// Update the note sprite's position.
noteSprite.updateNotePosition(renderedNotes);
}
}
@ -1839,8 +1910,11 @@ class ChartEditorState extends HaxeUIState
{
// Remember if we are already displaying this note.
if (displayedNoteData.indexOf(noteData) != -1)
{
continue;
}
// Get the position the note should be at.
var noteTimePixels:Float = noteData.time / Conductor.stepCrochet * GRID_SIZE;
// Make sure the note appears when scrolling up.
@ -1854,11 +1928,11 @@ class ChartEditorState extends HaxeUIState
// Get a note sprite from the pool.
// If we can reuse a deleted note, do so.
// If a new note is needed, call buildNoteSprite.
var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(ChartEditorNoteSprite);
var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this));
noteSprite.parentState = this;
// The note sprite handles animation playback and positioning.
noteSprite.noteData = noteData;
noteSprite.noteSkin = currentSongNoteSkin;
// Setting note data resets position relative to the grid so we fix that.
noteSprite.x += renderedNotes.x;
@ -1880,6 +1954,7 @@ class ChartEditorState extends HaxeUIState
}
var nextNoteSprite:ChartEditorNoteSprite = renderedNotes.recycle(ChartEditorNoteSprite);
nextNoteSprite.parentState = this;
nextNoteSprite.parentNoteSprite = lastNoteSprite;
lastNoteSprite.childNoteSprite = nextNoteSprite;
@ -2306,7 +2381,7 @@ class ChartEditorState extends HaxeUIState
var diffStepTime = Conductor.currentStepTime - oldStepTime;
// Move the playhead.
playheadPosition += diffStepTime * GRID_SIZE;
playheadPositionInPixels += diffStepTime * GRID_SIZE;
// We don't move the song to scroll position, or update the note sprites.
}
@ -2322,7 +2397,7 @@ class ChartEditorState extends HaxeUIState
// We need time in fractional steps here to allow the song to actually play.
// Also account for a potentially offset playhead.
scrollPosition = Conductor.currentStepTime * GRID_SIZE - playheadPosition;
scrollPositionInPixels = Conductor.currentStepTime * GRID_SIZE - playheadPositionInPixels;
// DO NOT move song to scroll position here specifically.
@ -2340,12 +2415,14 @@ class ChartEditorState extends HaxeUIState
/**
* Handle the playback of hitsounds.
*/
function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void {
function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void
{
if (!hitsoundsEnabled)
return;
// Assume notes are sorted by time.
for (noteData in currentSongChartNoteData) {
for (noteData in currentSongChartNoteData)
{
if (noteData.time < oldSongPosition)
// Note is in the past.
continue;
@ -2355,7 +2432,8 @@ class ChartEditorState extends HaxeUIState
return;
// Note was just hit.
switch (noteData.getStrumlineIndex()) {
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer)
playSound(Paths.sound('funnyNoise/funnyNoise-09'));
@ -2366,7 +2444,6 @@ class ChartEditorState extends HaxeUIState
}
}
function startAudioPlayback()
{
if (audioInstTrack != null)
@ -2429,40 +2506,40 @@ class ChartEditorState extends HaxeUIState
function placeNoteAtPlayhead(column:Int):Void
{
var gridSnappedPlayheadPos = scrollPosition - (scrollPosition % GRID_SIZE);
var gridSnappedPlayheadPos = scrollPositionInPixels - (scrollPositionInPixels % GRID_SIZE);
}
function set_scrollPosition(value:Float):Float
function set_scrollPositionInPixels(value:Float):Float
{
if (value < 0)
{
// If we're scrolling up, and we hit the top,
// but the playhead is in the middle, move the playhead up.
if (playheadPosition > 0)
if (playheadPositionInPixels > 0)
{
var amount = scrollPosition - value;
playheadPosition -= amount;
var amount = scrollPositionInPixels - value;
playheadPositionInPixels -= amount;
}
value = 0;
}
if (value > songLength)
value = songLength;
if (value > songLengthInPixels)
value = songLengthInPixels;
if (value == scrollPosition)
if (value == scrollPositionInPixels)
return value;
this.scrollPosition = value;
this.scrollPositionInPixels = value;
// Move the grid sprite to the correct position.
if (isViewDownscroll)
{
gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
}
else
{
gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
}
// Move the rendered notes to the correct position.
renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
@ -2474,23 +2551,28 @@ class ChartEditorState extends HaxeUIState
gridSpectrogram.setPosition(0, 0);
}
return this.scrollPosition;
return this.scrollPositionInPixels;
}
function set_playheadPosition(value:Float):Float
function get_playheadPositionInPixels():Float
{
return this.playheadPositionInPixels;
}
function set_playheadPositionInPixels(value:Float):Float
{
// Make sure playhead doesn't go outside the song.
if (value + scrollPosition < 0)
value = -scrollPosition;
if (value + scrollPosition > songLength)
value = songLength - scrollPosition;
if (value + scrollPositionInPixels < 0)
value = -scrollPositionInPixels;
if (value + scrollPositionInPixels > songLengthInPixels)
value = songLengthInPixels - scrollPositionInPixels;
this.playheadPosition = value;
this.playheadPositionInPixels = value;
// Move the playhead sprite to the correct position.
gridPlayhead.y = this.playheadPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
gridPlayhead.y = this.playheadPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
return this.playheadPosition;
return this.playheadPositionInPixels;
}
/**
@ -2557,17 +2639,17 @@ class ChartEditorState extends HaxeUIState
audioVocalTrackGroup.pause();
};
songLength = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * GRID_SIZE);
songLengthInPixels = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * GRID_SIZE);
gridTiledSprite.height = songLength;
gridTiledSprite.height = songLengthInPixels;
if (gridSpectrogram != null)
{
gridSpectrogram.setSound(audioInstTrack);
gridSpectrogram.generateSection(0, songLengthInMs / 1000);
}
scrollPosition = 0;
playheadPosition = 0;
scrollPositionInPixels = 0;
playheadPositionInPixels = 0;
moveSongToScrollPosition();
}
@ -2637,13 +2719,12 @@ class ChartEditorState extends HaxeUIState
this.songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
}
Conductor.forceBPM(null); // Disable the forced BPM.
Conductor.mapTimeChanges(currentSongMetadata.timeChanges);
loadInstrumentalFromAsset(Paths.inst(songId));
loadVocalsFromAsset(Paths.voices(songId));
// Apply BPM
// showNotification('Loaded song ${songId}.');
}

View file

@ -111,8 +111,8 @@ class ChartEditorThemeHandler
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
// This gets reused to fill the screen.
var gridWidth = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1);
var gridHeight = ChartEditorState.GRID_SIZE * (STEPS_PER_BEAT * BEATS_PER_MEASURE);
var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1));
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (STEPS_PER_BEAT * BEATS_PER_MEASURE));
state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1,
gridColor2);
@ -190,6 +190,11 @@ class ChartEditorThemeHandler
// Divider at 2 * (Strumline Size)
var dividerLineBX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.gridBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor);
if (state.gridTiledSprite != null) {
state.gridTiledSprite.loadGraphic(state.gridBitmap);
}
// Else, gridTiledSprite will be built later.
}
static function updateSelectionSquare(state:ChartEditorState):Void

View file

@ -0,0 +1,389 @@
package funkin.util;
import haxe.io.Path;
import haxe.io.Bytes;
import haxe.io.BytesOutput;
import haxe.io.Eof;
import haxe.zip.Entry;
import haxe.zip.Writer;
import haxe.Json;
import haxe.Template;
import sys.io.File;
import sys.io.Process;
import sys.FileSystem;
class SystemUtil
{
public static var hostArchitecture(get, never):HostArchitecture;
public static var hostPlatform(get, never):HostPlatform;
public static var processorCores(get, never):Int;
private static var _hostArchitecture:HostArchitecture;
private static var _hostPlatform:HostPlatform;
private static var _processorCores:Int = 0;
private static function get_hostPlatform():HostPlatform
{
if (_hostPlatform == null)
{
if (new EReg("window", "i").match(Sys.systemName()))
{
_hostPlatform = WINDOWS;
}
else if (new EReg("linux", "i").match(Sys.systemName()))
{
_hostPlatform = LINUX;
}
else if (new EReg("mac", "i").match(Sys.systemName()))
{
_hostPlatform = MAC;
}
trace("", " - \x1b[1mDetected host platform:\x1b[0m " + Std.string(_hostPlatform).toUpperCase());
}
return _hostPlatform;
}
private static function get_hostArchitecture():HostArchitecture
{
if (_hostArchitecture == null)
{
switch (hostPlatform)
{
case WINDOWS:
var architecture = Sys.getEnv("PROCESSOR_ARCHITECTURE");
var wow64Architecture = Sys.getEnv("PROCESSOR_ARCHITEW6432");
if (architecture.indexOf("64") > -1 || wow64Architecture != null && wow64Architecture.indexOf("64") > -1)
{
_hostArchitecture = X64;
}
else
{
_hostArchitecture = X86;
}
case LINUX, MAC:
#if nodejs
switch (js.Node.process.arch)
{
case "arm":
_hostArchitecture = ARMV7;
case "x64":
_hostArchitecture = X64;
default:
_hostArchitecture = X86;
}
#else
var process = new Process("uname", ["-m"]);
var output = process.stdout.readAll().toString();
var error = process.stderr.readAll().toString();
process.exitCode();
process.close();
if (output.indexOf("armv6") > -1)
{
_hostArchitecture = ARMV6;
}
else if (output.indexOf("armv7") > -1)
{
_hostArchitecture = ARMV7;
}
else if (output.indexOf("64") > -1)
{
_hostArchitecture = X64;
}
else
{
_hostArchitecture = X86;
}
#end
default:
_hostArchitecture = ARMV6;
}
trace("", " - \x1b[1mDetected host architecture:\x1b[0m " + Std.string(_hostArchitecture).toUpperCase());
}
return _hostArchitecture;
}
private static function get_processorCores():Int
{
if (_processorCores < 1)
{
var result = null;
if (hostPlatform == WINDOWS)
{
var env = Sys.getEnv("NUMBER_OF_PROCESSORS");
if (env != null)
{
result = env;
}
}
else if (hostPlatform == LINUX)
{
result = runProcess("", "nproc", null, true, true, true);
if (result == null)
{
var cpuinfo = runProcess("", "cat", ["/proc/cpuinfo"], true, true, true);
if (cpuinfo != null)
{
var split = cpuinfo.split("processor");
result = Std.string(split.length - 1);
}
}
}
else if (hostPlatform == MAC)
{
var cores = ~/Total Number of Cores: (\d+)/;
var output = runProcess("", "/usr/sbin/system_profiler", ["-detailLevel", "full", "SPHardwareDataType"]);
if (cores.match(output))
{
result = cores.matched(1);
}
}
if (result == null || Std.parseInt(result) < 1)
{
_processorCores = 1;
}
else
{
_processorCores = Std.parseInt(result);
}
}
return _processorCores;
}
public static function runProcess(path:String, command:String, args:Array<String> = null, waitForOutput:Bool = true, safeExecute:Bool = true,
ignoreErrors:Bool = false, print:Bool = false, returnErrorValue:Bool = false):String
{
if (print)
{
var message = command;
if (args != null)
{
for (arg in args)
{
if (arg.indexOf(" ") > -1)
{
message += " \"" + arg + "\"";
}
else
{
message += " " + arg;
}
}
}
Sys.println(message);
}
#if (haxe_ver < "3.3.0")
command = Path.escape(command);
#end
if (safeExecute)
{
try
{
if (path != null
&& path != ""
&& !FileSystem.exists(FileSystem.fullPath(path))
&& !FileSystem.exists(FileSystem.fullPath(new Path(path).dir)))
{
trace("The specified target path \"" + path + "\" does not exist");
}
return _runProcess(path, command, args, waitForOutput, safeExecute, ignoreErrors, returnErrorValue);
}
catch (e:Dynamic)
{
if (!ignoreErrors)
{
trace("", e);
}
return null;
}
}
else
{
return _runProcess(path, command, args, waitForOutput, safeExecute, ignoreErrors, returnErrorValue);
}
}
private static function _runProcess(path:String, command:String, args:Null<Array<String>>, waitForOutput:Bool, safeExecute:Bool, ignoreErrors:Bool,
returnErrorValue:Bool):String
{
var oldPath:String = "";
if (path != null && path != "")
{
trace("", " - \x1b[1mChanging directory:\x1b[0m " + path + "");
oldPath = Sys.getCwd();
Sys.setCwd(path);
}
var argString = "";
if (args != null)
{
for (arg in args)
{
if (arg.indexOf(" ") > -1)
{
argString += " \"" + arg + "\"";
}
else
{
argString += " " + arg;
}
}
}
trace("", " - \x1b[1mRunning process:\x1b[0m " + command + argString);
var output = "";
var result = 0;
var process:Process;
if (args != null && args.length > 0)
{
process = new Process(command, args);
}
else
{
process = new Process(command);
}
if (waitForOutput)
{
var buffer = new BytesOutput();
var waiting = true;
while (waiting)
{
try
{
var current = process.stdout.readAll(1024);
buffer.write(current);
if (current.length == 0)
{
waiting = false;
}
}
catch (e:Eof)
{
waiting = false;
}
}
result = process.exitCode();
output = buffer.getBytes().toString();
if (output == "")
{
var error = process.stderr.readAll().toString();
process.close();
if (result != 0 || error != "")
{
if (ignoreErrors)
{
output = error;
}
else if (!safeExecute)
{
throw error;
}
else
{
trace(error);
}
if (returnErrorValue)
{
return output;
}
else
{
return null;
}
}
/*if (error != "") {
trace (error);
}*/
}
else
{
process.close();
}
}
if (oldPath != "")
{
Sys.setCwd(oldPath);
}
return output;
}
public static function getTempDirectory(extension:String = ""):String
{
#if (flash || html5)
return null;
#else
var path = "";
if (hostPlatform == WINDOWS)
{
path = Sys.getEnv("TEMP");
}
else
{
path = Sys.getEnv("TMPDIR");
if (path == null)
{
path = "/tmp";
}
}
path = Path.join([path, "Funkin"]);
return path;
#end
}
}
enum HostArchitecture
{
ARMV6;
ARMV7;
X86;
X64;
}
@:enum abstract HostPlatform(String) from String to String
{
public var WINDOWS = "windows";
public var MAC = "mac";
public var LINUX = "linux";
}