From ba0660c2c0797afa1d146deb21e1b4c7f9acf51d Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 16 Jan 2024 08:58:31 -0500 Subject: [PATCH 01/39] ansi --- source/Main.hx | 2 + source/funkin/util/logging/AnsiTrace.hx | 120 ++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 source/funkin/util/logging/AnsiTrace.hx diff --git a/source/Main.hx b/source/Main.hx index 86e520e696..754d0732f0 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -33,6 +33,8 @@ class Main extends Sprite public static function main():Void { + haxe.Log.trace = funkin.util.logging.AnsiTrace.trace; + funkin.util.logging.AnsiTrace.traceBF(); Lib.current.addChild(new Main()); } diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx new file mode 100644 index 0000000000..993fcd4b92 --- /dev/null +++ b/source/funkin/util/logging/AnsiTrace.hx @@ -0,0 +1,120 @@ +package funkin.util.logging; + +class AnsiTrace +{ + // mostly a copy of haxe.Log.trace() + // but adds nice cute ANSI things + public static function trace(v:Dynamic, ?info:haxe.PosInfos) + { + var str = formatOutput(v, info); + #if js + if (js.Syntax.typeof(untyped console) != "undefined" && (untyped console).log != null) (untyped console).log(str); + #elseif lua + untyped __define_feature__("use._hx_print", _hx_print(str)); + #elseif sys + Sys.println(str); + #else + throw new haxe.exceptions.NotImplementedException() + #end + } + + public static var colorSupported:Bool = (Sys.getEnv("TERM") == "xterm" || Sys.getEnv("ANSICON") != null); + + // ansi stuff + public static inline var RED = "\x1b[31m"; + public static inline var YELLOW = "\x1b[33m"; + public static inline var WHITE = "\x1b[37m"; + public static inline var NORMAL = "\x1b[0m"; + public static inline var BOLD = "\x1b[1m"; + public static inline var ITALIC = "\x1b[3m"; + + // where the real mf magic happens with ansi stuff! + public static function formatOutput(v:Dynamic, infos:haxe.PosInfos):String + { + var str = Std.string(v); + if (infos == null) return str; + + if (colorSupported) + { + var dirs:Array = infos.fileName.split("/"); + dirs[dirs.length - 1] = ansiWrap(dirs[dirs.length - 1], BOLD); + + // rejoin the dirs + infos.fileName = dirs.join("/"); + } + + var pstr = infos.fileName + ":" + ansiWrap(infos.lineNumber, BOLD); + if (infos.customParams != null) for (v in infos.customParams) + str += ", " + Std.string(v); + return pstr + ": " + str; + } + + public static function traceBF() + { + #if sys + if (colorSupported) Sys.println(ansiBF.join("\n")); + #end + } + + public static function ansiWrap(str:Dynamic, ansiCol:String) + { + return ansify(ansiCol) + str + ansify(NORMAL); + } + + public static function ansify(ansiCol:String) + { + return (colorSupported ? ansiCol : ""); + } + + // generated using https://dom111.github.io/image-to-ansi/ + public static var ansiBF:Array = [ + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;184;46;83m \x1b[48;2;246;87;102m \x1b[48;2;239;83;100m \x1b[48;2;154;23;70m \x1b[48;2;154;23;69m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;191;52;87m \x1b[48;2;246;87;102m \x1b[48;2;241;84;100m \x1b[48;2;191;52;87m \x1b[48;2;153;23;69m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;234;94;114m \x1b[48;2;160;97;151m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;36;35;46m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;47;49;144m \x1b[48;2;246;87;102m \x1b[48;2;80;121;206m \x1b[48;2;193;167;177m \x1b[48;2;246;87;102m \x1b[48;2;184;46;83m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;47;49;144m \x1b[48;2;110;187;236m \x1b[48;2;109;66;125m \x1b[48;2;246;87;102m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;107;177;226m \x1b[48;2;234;94;114m \x1b[48;2;246;87;102m \x1b[48;2;237;81;99m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;106;196m \x1b[48;2;87;133;210m \x1b[48;2;64;105;174m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;57;65;148m \x1b[48;2;47;49;144m \x1b[48;2;141;248;252m \x1b[48;2;129;225;245m \x1b[48;2;157;94;147m \x1b[48;2;246;87;102m \x1b[48;2;159;27;72m \x1b[48;2;205;63;93m \x1b[48;2;55;27;43m \x1b[48;2;21;21;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;125;216;244m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;60;97;187m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;104;173;229m \x1b[48;2;146;68;123m \x1b[48;2;246;87;102m \x1b[48;2;180;44;82m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;104;173;229m \x1b[48;2;146;68;123m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;78;118;190m \x1b[48;2;239;83;100m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;78;118;190m \x1b[48;2;239;83;100m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;51;72;160m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;117;138;166m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;55;134;228m \x1b[48;2;110;187;236m \x1b[48;2;139;244;251m \x1b[48;2;205;63;93m \x1b[48;2;123;4;53m \x1b[48;2;125;6;54m \x1b[48;2;146;23;68m \x1b[48;2;205;63;93m \x1b[48;2;123;4;53m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;103;130;185m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;107;177;226m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;58;123;219m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;141;248;252m \x1b[48;2;135;237;249m \x1b[48;2;157;140;181m \x1b[48;2;44;30;46m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;36;35;46m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;93;37;66m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;240;174;162m \x1b[48;2;130;96;96m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;170;35;77m \x1b[48;2;196;126;137m \x1b[48;2;249;225;202m \x1b[48;2;132;70;100m \x1b[48;2;20;19;31m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;138;242;250m \x1b[48;2;74;107;200m \x1b[49m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;20;19;31m \x1b[48;2;175;111;124m \x1b[48;2;235;169;159m \x1b[48;2;240;174;162m \x1b[48;2;227;160;155m \x1b[48;2;20;19;31m \x1b[48;2;232;165;158m \x1b[48;2;240;174;162m \x1b[48;2;85;109;196m \x1b[48;2;138;242;250m \x1b[48;2;112;191;237m \x1b[48;2;104;181;235m \x1b[48;2;110;187;236m \x1b[48;2;23;22;43m \x1b[48;2;26;23;37m \x1b[48;2;249;225;202m \x1b[48;2;248;220;198m \x1b[48;2;249;225;202m \x1b[48;2;137;90;124m \x1b[48;2;51;112;205m \x1b[48;2;53;128;224m \x1b[48;2;23;25;44m \x1b[48;2;18;18;28m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;109;182;229m \x1b[48;2;74;107;200m \x1b[49m \x1b[48;2;151;78;111m \x1b[48;2;194;126;136m \x1b[48;2;110;100;98m \x1b[48;2;244;194;178m \x1b[48;2;72;42;63m \x1b[48;2;103;76;81m \x1b[48;2;191;136;147m \x1b[48;2;240;174;162m \x1b[48;2;206;136;142m \x1b[48;2;20;19;31m \x1b[48;2;232;165;158m \x1b[48;2;240;174;162m \x1b[48;2;65;128;218m \x1b[48;2;141;248;252m \x1b[48;2;74;107;200m \x1b[48;2;85;133;200m \x1b[48;2;88;139;214m \x1b[48;2;84;69;150m \x1b[48;2;211;167;166m \x1b[48;2;187;116;132m \x1b[48;2;213;145;147m \x1b[48;2;245;205;186m \x1b[48;2;135;83;115m \x1b[48;2;43;66;124m \x1b[48;2;47;87;174m \x1b[48;2;51;130;227m \x1b[48;2;28;40;76m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;71;103;199m \x1b[48;2;73;105;197m \x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;246;209;189m \x1b[48;2;249;225;202m \x1b[48;2;226;159;154m \x1b[48;2;244;215;196m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;226;159;154m \x1b[48;2;142;54;93m \x1b[48;2;245;213;193m \x1b[48;2;249;225;202m \x1b[48;2;213;185;192m \x1b[48;2;85;132;211m \x1b[48;2;222;158;164m \x1b[48;2;183;111;129m \x1b[48;2;110;187;236m \x1b[48;2;171;158;211m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;240;174;162m \x1b[48;2;166;93;120m \x1b[48;2;130;70;98m \x1b[48;2;19;19;31m \x1b[48;2;29;34;56m \x1b[48;2;55;93;183m \x1b[48;2;68;101;193m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;196;126;137m \x1b[48;2;218;150;149m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[48;2;47;49;144m \x1b[48;2;196;126;137m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;20;19;31m \x1b[48;2;41;43;121m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;244;215;196m \x1b[48;2;249;225;202m \x1b[48;2;145;49;90m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;249;225;202m \x1b[48;2;200;131;139m \x1b[48;2;249;225;202m \x1b[48;2;154;63;98m \x1b[48;2;145;49;90m \x1b[48;2;240;174;162m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;210;141;145m \x1b[48;2;241;181;168m \x1b[48;2;249;225;202m \x1b[48;2;196;126;137m \x1b[48;2;195;125;136m \x1b[48;2;170;94;119m \x1b[48;2;237;73;115m \x1b[48;2;244;75;120m \x1b[48;2;145;49;90m \x1b[48;2;249;225;202m \x1b[48;2;241;181;167m \x1b[48;2;181;121;161m \x1b[48;2;255;224;255m \x1b[48;2;178;117;159m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;136;72;102m \x1b[48;2;190;119;133m \x1b[48;2;171;99;123m \x1b[48;2;152;74;109m \x1b[48;2;244;75;120m \x1b[48;2;145;49;90m \x1b[48;2;190;119;133m \x1b[48;2;185;128;172m \x1b[48;2;180;121;164m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;147;80;107m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;55;73;115m \x1b[48;2;36;35;46m \x1b[48;2;251;117;129m \x1b[48;2;205;63;93m \x1b[48;2;230;143;174m \x1b[48;2;255;224;255m \x1b[48;2;145;49;90m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;251;219;252m \x1b[48;2;105;60;85m \x1b[48;2;36;35;46m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;156;82;114m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;65;84;125m \x1b[48;2;98;121;155m \x1b[48;2;124;146;175m \x1b[48;2;194;215;238m \x1b[48;2;50;68;111m \x1b[48;2;124;146;175m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;254;224;245m \x1b[48;2;231;143;176m \x1b[48;2;255;224;255m \x1b[48;2;145;49;90m \x1b[48;2;255;224;255m \x1b[48;2;72;85;110m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;65;84;125m \x1b[48;2;50;68;111m \x1b[48;2;124;146;175m \x1b[48;2;78;99;137m \x1b[48;2;194;215;238m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;254;224;245m \x1b[48;2;253;170;192m \x1b[48;2;255;224;255m \x1b[48;2;251;117;129m \x1b[48;2;255;224;255m \x1b[48;2;170;105;144m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;76;111m \x1b[48;2;165;91;118m \x1b[48;2;180;108;128m \x1b[48;2;250;227;206m \x1b[48;2;116;138;169m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;116;138;169m \x1b[48;2;172;193;218m \x1b[48;2;168;206;237m \x1b[48;2;49;62;121m \x1b[48;2;73;92;131m \x1b[48;2;115;137;168m \x1b[48;2;116;138;169m \x1b[48;2;124;146;175m \x1b[48;2;50;40;54m \x1b[48;2;57;43;58m \x1b[48;2;251;170;183m \x1b[48;2;255;206;227m \x1b[48;2;251;117;129m \x1b[48;2;252;132;140m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[48;2;243;190;174m \x1b[48;2;249;226;203m \x1b[48;2;196;126;137m \x1b[48;2;44;70;156m \x1b[48;2;47;86;175m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;239;198;185m \x1b[48;2;250;227;206m \x1b[48;2;244;195;179m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;166;188;213m \x1b[48;2;36;35;46m \x1b[48;2;46;58;91m \x1b[48;2;79;100;138m \x1b[48;2;71;79;97m \x1b[48;2;60;69;89m \x1b[48;2;136;158;188m \x1b[48;2;117;140;170m \x1b[48;2;36;35;46m \x1b[48;2;71;79;97m \x1b[48;2;79;100;138m \x1b[48;2;62;51;68m \x1b[48;2;246;192;224m \x1b[48;2;253;182;205m \x1b[48;2;255;224;255m \x1b[48;2;144;140;167m \x1b[48;2;82;52;72m \x1b[48;2;120;110;108m \x1b[48;2;250;227;206m \x1b[48;2;229;187;179m \x1b[48;2;169;166;186m \x1b[48;2;47;64;156m \x1b[48;2;46;87;175m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;220;168;164m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;194;215;238m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;95;115;147m \x1b[48;2;93;115;150m \x1b[48;2;50;68;111m \x1b[48;2;37;37;48m \x1b[48;2;51;44;59m \x1b[48;2;66;116;206m \x1b[48;2;47;83;172m \x1b[48;2;68;134;227m \x1b[48;2;250;227;206m \x1b[48;2;61;91;174m \x1b[48;2;47;85;173m \x1b[48;2;47;87;175m \x1b[48;2;47;86;175m \x1b[49m \x1b[m", + "\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;217;165;163m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;82;52;72m \x1b[48;2;194;215;238m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;38;41;58m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;49;144m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m", + "\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;242;185;171m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;217;165;163m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;36;35;46m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;38;41;58m \x1b[48;2;47;68;159m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m", + "\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;242;185;171m \x1b[48;2;196;126;137m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;206;136;142m \x1b[48;2;227;160;155m \x1b[48;2;240;174;162m \x1b[48;2;82;52;72m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;153;55;95m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;152;77;112m \x1b[48;2;228;161;155m \x1b[48;2;237;171;160m \x1b[48;2;196;126;137m \x1b[48;2;193;123;135m \x1b[48;2;177;105;126m \x1b[48;2;193;123;135m \x1b[48;2;37;37;50m \x1b[48;2;155;69;105m \x1b[48;2;36;35;46m \x1b[48;2;49;66;107m \x1b[48;2;36;35;46m \x1b[48;2;159;83;115m \x1b[48;2;155;67;103m \x1b[48;2;47;69;142m \x1b[48;2;47;87;175m \x1b[48;2;46;73;157m \x1b[48;2;47;85;173m \x1b[48;2;63;77;159m \x1b[48;2;247;97;126m \x1b[48;2;254;165;165m \x1b[48;2;253;160;161m \x1b[48;2;159;79;111m \x1b[48;2;72;106;198m \x1b[48;2;50;67;149m \x1b[48;2;53;69;151m \x1b[48;2;157;77;111m \x1b[48;2;151;22;68m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;147;80;107m \x1b[48;2;176;104;125m \x1b[48;2;240;174;162m \x1b[48;2;66;46;63m \x1b[48;2;36;35;46m \x1b[48;2;170;35;77m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;201;153;202m \x1b[48;2;246;87;102m \x1b[48;2;245;171;163m \x1b[48;2;253;156;159m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;47;87;175m \x1b[48;2;154;23;70m \x1b[48;2;246;170;163m \x1b[48;2;254;165;165m \x1b[48;2;246;87;102m \x1b[48;2;245;79;114m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;219;68;93m \x1b[48;2;118;0;50m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;147;80;107m \x1b[48;2;187;116;132m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;251;127;141m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;225;75;97m \x1b[48;2;154;23;70m \x1b[48;2;159;27;72m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;118;0;50m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;151;78;111m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;200;151;200m \x1b[48;2;216;175;226m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;246;85;105m \x1b[48;2;205;63;93m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;159;27;72m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;144;16;64m \x1b[48;2;118;0;50m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;205;63;93m \x1b[48;2;159;27;72m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;168;74;106m \x1b[48;2;174;39;79m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;211;70;106m \x1b[48;2;233;196;238m \x1b[48;2;255;224;255m \x1b[48;2;178;42;81m \x1b[48;2;205;157;200m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;178;42;81m \x1b[48;2;154;23;70m \x1b[48;2;153;52;92m \x1b[48;2;136;40;82m \x1b[48;2;144;17;63m \x1b[48;2;225;75;97m \x1b[48;2;234;198;240m \x1b[48;2;255;224;255m \x1b[48;2;234;198;240m \x1b[48;2;233;196;238m \x1b[48;2;239;204;243m \x1b[48;2;212;165;204m \x1b[48;2;245;87;103m \x1b[48;2;180;44;82m \x1b[48;2;180;70;102m \x1b[48;2;152;77;112m \x1b[48;2;151;76;110m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;68;104m \x1b[48;2;213;67;95m \x1b[48;2;246;87;102m \x1b[48;2;213;67;95m \x1b[48;2;195;66;97m \x1b[48;2;173;61;105m \x1b[48;2;154;23;70m \x1b[48;2;195;55;89m \x1b[48;2;205;63;93m \x1b[48;2;125;4;54m \x1b[48;2;170;101;145m \x1b[48;2;171;102;146m \x1b[48;2;201;153;202m \x1b[48;2;182;124;167m \x1b[48;2;158;87;122m \x1b[48;2;138;36;79m \x1b[48;2;205;63;93m \x1b[48;2;196;143;183m \x1b[48;2;255;224;255m \x1b[48;2;234;94;114m \x1b[48;2;229;85;104m \x1b[48;2;236;96;117m \x1b[48;2;248;113;131m \x1b[48;2;246;88;103m \x1b[48;2;246;87;102m \x1b[48;2;195;66;97m \x1b[48;2;172;109;148m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;182;124;167m \x1b[48;2;197;147;195m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[48;2;142;44;86m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;255;224;255m \x1b[48;2;225;187;233m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;200;151;200m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;182;124;167m \x1b[48;2;147;64;101m \x1b[48;2;36;35;46m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;200;100;119m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;200;151;200m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;36;35;46m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;153;55;95m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;255;224;255m \x1b[48;2;176;115;156m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;185;128;172m \x1b[48;2;153;78;112m \x1b[48;2;36;35;46m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;153;55;95m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;176;115;156m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;87;54;75m \x1b[48;2;207;163;214m \x1b[48;2;190;136;182m \x1b[48;2;202;151;191m \x1b[48;2;244;187;187m \x1b[48;2;241;206;245m \x1b[48;2;243;209;246m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;56;42;57m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;243;208;246m \x1b[48;2;238;203;242m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[48;2;153;77;111m \x1b[49m \x1b[m", + "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[49m \x1b[m" + ]; +} From 608d9b6968df21423146f6448f79e35172d65ac7 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 20 Jan 2024 14:07:31 -0500 Subject: [PATCH 02/39] Add new Offsets window, work in progress --- source/funkin/InitState.hx | 2 + source/funkin/audio/FunkinSound.hx | 13 ++++ .../audio/visualize/PolygonSpectogram.hx | 2 +- .../funkin/graphics/rendering/MeshRender.hx | 77 ++++++++++++------- .../ui/debug/charting/ChartEditorState.hx | 9 ++- .../handlers/ChartEditorThemeHandler.hx | 55 +++++++++++++ .../handlers/ChartEditorToolboxHandler.hx | 18 ++++- .../toolboxes/ChartEditorOffsetsToolbox.hx | 63 +++++++++++++++ source/funkin/util/logging/CrashHandler.hx | 12 +++ 9 files changed, 216 insertions(+), 35 deletions(-) create mode 100644 source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index c9198c3d47..23bc255f11 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -243,6 +243,8 @@ class InitState extends FlxState FlxG.switchState(new FreeplayState()); #elseif ANIMATE // -DANIMATE FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest()); + #elseif WAVEFORM // -DWAVEFORM + FlxG.switchState(new funkin.ui.debug.WaveformTestState()); #elseif CHARTING // -DCHARTING FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState()); #elseif STAGEBUILD // -DSTAGEBUILD diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 278578fb3d..acae9c6389 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -63,6 +63,19 @@ class FunkinSound extends FlxSound } } + public function togglePlayback():FunkinSound + { + if (playing) + { + pause(); + } + else + { + resume(); + } + return this; + } + public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound { if (!exists) return this; diff --git a/source/funkin/audio/visualize/PolygonSpectogram.hx b/source/funkin/audio/visualize/PolygonSpectogram.hx index 37a6c15d1d..948027a8d5 100644 --- a/source/funkin/audio/visualize/PolygonSpectogram.hx +++ b/source/funkin/audio/visualize/PolygonSpectogram.hx @@ -102,7 +102,7 @@ class PolygonSpectogram extends MeshRender coolPoint.x = (curAud.balanced * waveAmplitude); coolPoint.y = (i / funnyPixels * daHeight); - add_quad(prevPoint.x, prevPoint.y, prevPoint.x + build_quad(prevPoint.x, prevPoint.y, prevPoint.x + thickness, prevPoint.y, coolPoint.x, coolPoint.y, coolPoint.x + thickness, coolPoint.y + thickness); diff --git a/source/funkin/graphics/rendering/MeshRender.hx b/source/funkin/graphics/rendering/MeshRender.hx index 39402808a9..6a781beae9 100644 --- a/source/funkin/graphics/rendering/MeshRender.hx +++ b/source/funkin/graphics/rendering/MeshRender.hx @@ -12,22 +12,19 @@ class MeshRender extends FlxStrip public var vertex_count(default, null):Int = 0; public var index_count(default, null):Int = 0; - var tri_offset:Int = 0; - public function new(x, y, ?col:FlxColor = FlxColor.WHITE) { super(x, y); makeGraphic(1, 1, col); } - public inline function start() - { - tri_offset = vertex_count; - } - - public inline function add_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0) + /** + * Add a vertex. + */ + public inline function build_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0):Int { - final pos = vertex_count << 1; + final index = vertex_count; + final pos = index << 1; vertices[pos] = x; vertices[pos + 1] = y; @@ -36,42 +33,64 @@ class MeshRender extends FlxStrip uvtData[pos + 1] = v; vertex_count++; + return index; } - public function add_tri(a:Int, b:Int, c:Int) + /** + * Build a triangle from three vertex indexes. + * @param a + * @param b + * @param c + */ + public function add_tri(a:Int, b:Int, c:Int):Void { - indices[index_count] = a + tri_offset; - indices[index_count + 1] = b + tri_offset; - indices[index_count + 2] = c + tri_offset; + indices[index_count] = a; + indices[index_count + 1] = b; + indices[index_count + 2] = c; index_count += 3; } + public function build_tri(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, au:Float = 0, av:Float = 0, bu:Float = 0, bv:Float = 0, cu:Float = 0, + cv:Float = 0):Void + { + add_tri(build_vertex(ax, ay, au, av), build_vertex(bx, by, bu, bv), build_vertex(cx, cy, cu, cv)); + } + /** + * @param a top left vertex + * @param b top right vertex + * @param c bottom right vertex + * @param d bottom left vertex + */ + public function add_quad(a:Int, b:Int, c:Int, d:Int):Void + { + add_tri(a, b, c); + add_tri(a, c, d); + } + + /** + * Build a quad from four points. * - * top left - a - * - * top right - b - * - * bottom left - c - * - * bottom right - d + * top right - a + * top left - b + * bottom right - c + * bottom left - d */ - public function add_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0, - bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0) + public function build_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0, + bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0):Void { - start(); // top left - add_vertex(bx, by, bu, bv); + var b = build_vertex(bx, by, bu, bv); // top right - add_vertex(ax, ay, au, av); + var a = build_vertex(ax, ay, au, av); // bottom left - add_vertex(cx, cy, cu, cv); + var c = build_vertex(cx, cy, cu, cv); // bottom right - add_vertex(dx, dy, du, dv); + var d = build_vertex(dx, dy, du, dv); - add_tri(0, 1, 2); - add_tri(0, 2, 3); + add_tri(a, b, c); + add_tri(a, c, d); } public function clear() diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index c9f1e41370..fbb1c9743d 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -159,6 +159,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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'); @@ -1820,11 +1821,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var notePreviewViewportBitmap:Null = null; - /**r + /** * The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler. */ var measureTickBitmap:Null = null; + /** + * The IMAGE used for the offset ticks. Updated by ChartEditorThemeHandler. + */ + var offsetTickBitmap:Null = null; + /** * The tiled sprite used to display the grid. * The height is the length of the song, and scrolling is done by simply the sprite. @@ -2893,6 +2899,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value); menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value); + 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); menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index 89fd4d5d34..9479aa8402 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -82,6 +82,7 @@ class ChartEditorThemeHandler updateBackground(state); updateGridBitmap(state); updateMeasureTicks(state); + updateOffsetTicks(state); updateSelectionSquare(state); updateNotePreview(state); } @@ -231,6 +232,9 @@ class ChartEditorThemeHandler // Else, gridTiledSprite will be built later. } + /** + * Vertical measure ticks. + */ static function updateMeasureTicks(state:ChartEditorState):Void { var measureTickWidth:Int = 6; @@ -286,6 +290,57 @@ class ChartEditorThemeHandler state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); } + /** + * Horizontal offset ticks. + */ + static function updateOffsetTicks(state:ChartEditorState):Void + { + var majorTickWidth:Int = 6; + var minorTickWidth:Int = 3; + + var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 10 minor ticks wide. + var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares tall. + state.offsetTickBitmap = new BitmapData(ticksWidth, ticksHeight, true); + state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK); + + // Draw the major ticks. + var leftTickX:Float = 0; + var middleTickX:Float = state.offsetTickBitmap.width / 2 - (majorTickWidth / 2); + var rightTickX:Float = state.offsetTickBitmap.width - majorTickWidth; + state.offsetTickBitmap.fillRect(new Rectangle(leftTickX, 0, majorTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(middleTickX, 0, majorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, majorTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + + // Draw the minor ticks. + var minorTick2X:Float = state.offsetTickBitmap.width * 1 / 10 - (minorTickWidth / 2); + var minorTick3X:Float = state.offsetTickBitmap.width * 2 / 10 - (minorTickWidth / 2); + var minorTick4X:Float = state.offsetTickBitmap.width * 3 / 10 - (minorTickWidth / 2); + var minorTick5X:Float = state.offsetTickBitmap.width * 4 / 10 - (minorTickWidth / 2); + var minorTick7X:Float = state.offsetTickBitmap.width * 6 / 10 - (minorTickWidth / 2); + var minorTick8X:Float = state.offsetTickBitmap.width * 7 / 10 - (minorTickWidth / 2); + var minorTick9X:Float = state.offsetTickBitmap.width * 8 / 10 - (minorTickWidth / 2); + var minorTick10X:Float = state.offsetTickBitmap.width * 9 / 10 - (minorTickWidth / 2); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick2X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick3X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick4X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick5X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick7X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick8X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick9X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick10X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + + // Draw the offset ticks. + // var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); // 1 grid squares wide. + // var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 measure tall. + // state.offsetTickBitmap = new BitmapData(ticksWidth, ticksHeight, true); + // state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK); + // + //// Draw the offset ticks. + // state.offsetTickBitmap.fillRect(new Rectangle(0, 0, offsetTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + // var rightTickX:Float = state.offsetTickBitmap.width - (offsetTickWidth / 2); + // state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, offsetTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + } + static function updateSelectionSquare(state:ChartEditorState):Void { var selectionSquareBorderColor:FlxColor = switch (state.currentTheme) diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index f97b26933d..b246e653fe 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -35,6 +35,7 @@ import haxe.ui.containers.dialogs.Dialog.DialogButton; 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.ChartEditorEventDataToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; import haxe.ui.containers.Frame; @@ -89,6 +90,8 @@ class ChartEditorToolboxHandler case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT: // TODO: Fix this. cast(toolbox, ChartEditorBaseToolbox).refresh(); + case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT: + cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: onShowToolboxPlayerPreview(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: @@ -124,8 +127,6 @@ class ChartEditorToolboxHandler onHideToolboxEventData(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: onHideToolboxPlaytestProperties(state, toolbox); - case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT: - onHideToolboxMetadata(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: onHideToolboxPlayerPreview(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: @@ -202,6 +203,8 @@ class ChartEditorToolboxHandler toolbox = buildToolboxDifficultyLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT: toolbox = buildToolboxMetadataLayout(state); + case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT: + toolbox = buildToolboxOffsetsLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: toolbox = buildToolboxPlayerPreviewLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: @@ -304,8 +307,6 @@ class ChartEditorToolboxHandler static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} @@ -373,6 +374,15 @@ class ChartEditorToolboxHandler return toolbox; } + static function buildToolboxOffsetsLayout(state:ChartEditorState):Null + { + var toolbox:ChartEditorBaseToolbox = ChartEditorOffsetsToolbox.build(state); + + if (toolbox == null) return null; + + return toolbox; + } + static function buildToolboxEventDataLayout(state:ChartEditorState):Null { var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state); diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx new file mode 100644 index 0000000000..a68b8d364c --- /dev/null +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -0,0 +1,63 @@ +package funkin.ui.debug.charting.toolboxes; + +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; +import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; +import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import haxe.ui.components.Button; +import haxe.ui.components.CheckBox; +import haxe.ui.components.DropDown; +import haxe.ui.components.HorizontalSlider; +import haxe.ui.components.Label; +import haxe.ui.components.NumberStepper; +import haxe.ui.components.Slider; +import haxe.ui.components.TextField; +import funkin.play.stage.Stage; +import haxe.ui.containers.Box; +import haxe.ui.containers.Frame; +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 +{ + 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; + + refresh(); + } + + public override function refresh():Void + { + super.refresh(); + } + + public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox + { + return new ChartEditorOffsetsToolbox(chartEditorState); + } +} diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index e32ba2c420..82db68d30b 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -3,6 +3,7 @@ package funkin.util.logging; import openfl.Lib; import openfl.events.UncaughtErrorEvent; import flixel.util.FlxSignal.FlxTypedSignal; +import flixel.FlxG.FlxRenderMethod; /** * A custom crash handler that writes to a log file and displays a message box. @@ -118,6 +119,7 @@ class CrashHandler var driverInfo = FlxG?.stage?.context3D?.driverInfo ?? 'N/A'; fullContents += 'Driver info: ${driverInfo}\n'; fullContents += 'Platform: ${Sys.systemName()}\n'; + fullContents += 'Render method: ${renderMethod()}\n'; fullContents += '\n'; @@ -196,4 +198,14 @@ class CrashHandler { throw "This is an example of an uncaught exception."; } + + static function renderMethod():String + { + return switch (FlxG.renderMethod) + { + case FlxRenderMethod.DRAW_TILES: 'DRAW_TILES'; + case FlxRenderMethod.BLITTING: 'BLITTING'; + default: 'UNKNOWN'; + } + } } From c3d25822522cb6b3e0cd37f4b7672b8fc9256954 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 20 Jan 2024 14:07:48 -0500 Subject: [PATCH 03/39] Work in progress on new Waveform renderer --- .vscode/settings.json | 5 + assets | 2 +- source/funkin/audio/waveform/WaveformData.hx | 243 ++++++++++++++++++ .../audio/waveform/WaveformDataParser.hx | 32 +++ source/funkin/ui/debug/WaveformTestState.hx | 202 +++++++++++++++ 5 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 source/funkin/audio/waveform/WaveformData.hx create mode 100644 source/funkin/audio/waveform/WaveformDataParser.hx create mode 100644 source/funkin/ui/debug/WaveformTestState.hx diff --git a/.vscode/settings.json b/.vscode/settings.json index cefbadcf68..8979e4de63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -125,6 +125,11 @@ "target": "windows", "args": ["-debug", "-DLATENCY"] }, + { + "label": "Windows / Debug (Waveform Test)", + "target": "windows", + "args": ["-debug", "-DWAVEFORM"] + }, { "label": "HTML5 / Debug", "target": "html5", diff --git a/assets b/assets index 7e19c4cfa7..3d92b49768 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 7e19c4cfa7db57178f03ed4a58a9fd4d2b93dea7 +Subproject commit 3d92b497682727d34eaa55e564e0bd9faea1c9d7 diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx new file mode 100644 index 0000000000..525d1bd5fb --- /dev/null +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -0,0 +1,243 @@ +package funkin.audio.waveform; + +import funkin.util.MathUtil; + +@:nullSafety +class WaveformData +{ + /** + * The version of the waveform data format. + * @default `2` (-1 if not specified/invalid) + */ + public var version(default, null):Int = -1; + + /** + * The number of channels in the waveform. + */ + public var channels(default, null):Int = 1; + + @:alias('sample_rate') + public var sampleRate(default, null):Int = 44100; + + /** + * Number of input audio samples per output waveform data point. + * At base zoom level this is number of samples per pixel. + * Lower values can more accurately represent the waveform when zoomed in, but take more data. + */ + @:alias('samples_per_pixel') + public var samplesPerPixel(default, null):Int = 256; + + /** + * Number of bits to use for each sample value. Valid values are `8` and `16`. + */ + public var bits(default, null):Int = 16; + + /** + * Number of output waveform data points. + */ + public var length(default, null):Int = 0; // Array size is (4 * length) + + /** + * Array of Int16 values representing the waveform. + * TODO: Use an `openfl.Vector` for performance. + */ + public var data(default, null):Array = []; + + @:jignored + var channelData:Null> = null; + + public function new() {} + + function buildChannelData():Array + { + channelData = []; + for (i in 0...channels) + { + channelData.push(new WaveformDataChannel(this, i)); + } + return channelData; + } + + public function channel(index:Int) + { + return (channelData == null) ? buildChannelData()[index] : channelData[index]; + } + + public function get(index:Int):Int + { + return data[index] ?? 0; + } + + public function set(index:Int, value:Int) + { + data[index] = value; + } + + /** + * Maximum possible value for a waveform data point. + * The minimum possible value is (-1 * maxSampleValue) + */ + public function maxSampleValue():Int + { + if (_maxSampleValue != -1) return _maxSampleValue; + return _maxSampleValue = Std.int(Math.pow(2, bits)); + } + + /** + * Cache the value because `Math.pow` is expensive and the value gets used a lot. + */ + @:jignored + var _maxSampleValue:Int = -1; + + /** + * @return The length of the waveform in samples. + */ + public function lenSamples():Int + { + return length * samplesPerPixel; + } + + /** + * @return The length of the waveform in seconds. + */ + public function lenSeconds():Float + { + return lenSamples() / sampleRate; + } + + /** + * Given the time in seconds, return the waveform data point index. + */ + public function secondsToIndex(seconds:Float):Int + { + return Std.int(seconds * sampleRate / samplesPerPixel); + } + + /** + * Given a waveform data point index, return the time in seconds. + */ + public function indexToSeconds(index:Int):Float + { + return index * samplesPerPixel / sampleRate; + } + + /** + * Given the percentage progress through the waveform, return the waveform data point index. + */ + public function percentToIndex(percent:Float):Int + { + return Std.int(percent * length); + } + + /** + * Given a waveform data point index, return the percentage progress through the waveform. + */ + public function indexToPercent(index:Int):Float + { + return index / length; + } +} + +class WaveformDataChannel +{ + var parent:WaveformData; + var channelId:Int; + + public function new(parent:WaveformData, channelId:Int) + { + this.parent = parent; + this.channelId = channelId; + } + + public function minSample(i:Int) + { + var offset = (i * parent.channels + this.channelId) * 2; + return parent.get(offset); + } + + /** + * Mapped to a value between 0 and 1. + */ + public function minSampleMapped(i:Int) + { + return minSample(i) / parent.maxSampleValue(); + } + + /** + * Minimum value within the range of samples. + * @param i + */ + public function minSampleRange(start:Int, end:Int) + { + var min = parent.maxSampleValue(); + for (i in start...end) + { + var sample = minSample(i); + if (sample < min) min = sample; + } + return min; + } + + /** + * Maximum value within the range of samples, mapped to a value between 0 and 1. + * @param i + */ + public function minSampleRangeMapped(start:Int, end:Int) + { + return minSampleRange(start, end) / parent.maxSampleValue(); + } + + public function maxSample(i:Int) + { + var offset = (i * parent.channels + this.channelId) * 2 + 1; + return parent.get(offset); + } + + /** + * Mapped to a value between 0 and 1. + */ + public function maxSampleMapped(i:Int) + { + return maxSample(i) / parent.maxSampleValue(); + } + + /** + * Maximum value within the range of samples. + * @param i + */ + public function maxSampleRange(start:Int, end:Int) + { + var max = -parent.maxSampleValue(); + for (i in start...end) + { + var sample = maxSample(i); + if (sample > max) max = sample; + } + return max; + } + + /** + * Maximum value within the range of samples, mapped to a value between 0 and 1. + * @param i + */ + public function maxSampleRangeMapped(start:Int, end:Int) + { + return maxSampleRange(start, end) / parent.maxSampleValue(); + } + + /** + * Maximum value within the range of samples, mapped to a value between 0 and 1. + * @param i + */ + public function setMinSample(i:Int, value:Int) + { + var offset = (i * parent.channels + this.channelId) * 2; + parent.set(offset, value); + } + + public function setMaxSample(i:Int, value:Int) + { + var offset = (i * parent.channels + this.channelId) * 2 + 1; + parent.set(offset, value); + } +} diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx new file mode 100644 index 0000000000..fb453cf1b6 --- /dev/null +++ b/source/funkin/audio/waveform/WaveformDataParser.hx @@ -0,0 +1,32 @@ +package funkin.audio.waveform; + +class WaveformDataParser +{ + public static function parseWaveformData(path:String):Null + { + var rawJson:String = openfl.Assets.getText(path).trim(); + return parseWaveformDataString(rawJson, path); + } + + public static function parseWaveformDataString(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + static function printErrors(errors:Array, id:String = ''):Void + { + trace('[WAVEFORM] Failed to parse waveform data: ${id}'); + + for (error in errors) + funkin.data.DataError.printError(error); + } +} diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx new file mode 100644 index 0000000000..b202116b95 --- /dev/null +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -0,0 +1,202 @@ +package funkin.ui.debug; + +import flixel.FlxSprite; +import flixel.util.FlxColor; +import funkin.audio.FunkinSound; +import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformDataParser; +import funkin.graphics.rendering.MeshRender; + +class WaveformTestState extends MusicBeatState +{ + public function new() + { + super(); + } + + var waveformData:WaveformData; + + var waveformAudio:FunkinSound; + + var meshRender:MeshRender; + + var timeMarker:FlxSprite; + + public override function create():Void + { + super.create(); + + waveformData = WaveformDataParser.parseWaveformData(Paths.json("waveform/dadbattle-erect/dadbattle-erect.waveform")); + + waveformAudio = FunkinSound.load(Paths.music('dadbattle-erect/dadbattle-erect')); + + var lightBlue:FlxColor = FlxColor.fromString("#ADD8E6"); + meshRender = new MeshRender(0, 0, lightBlue); + add(meshRender); + + timeMarker = new FlxSprite(0, FlxG.height * 1 / 6); + timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED); + add(timeMarker); + + drawWaveform(time, duration); + } + + /** + * @param offsetX Horizontal offset to draw the waveform at, in samples. + */ + function drawWaveform(timeSeconds:Float, duration:Float):Void + { + meshRender.clear(); + + var offsetX:Int = waveformData.secondsToIndex(timeSeconds); + + var waveformHeight:Int = Std.int(FlxG.height * (2 / 3)); + var waveformWidth:Int = FlxG.width; + var waveformCenterPos:Int = Std.int(FlxG.height / 2); + + var oneSecondInIndices:Int = waveformData.secondsToIndex(1); + + var startTime:Float = -1.0; + var endTime:Float = startTime + duration; + + var startIndex:Int = Std.int(offsetX + (oneSecondInIndices * startTime)); + var endIndex:Int = Std.int(offsetX + (oneSecondInIndices * (startTime + duration))); + + var pixelsPerIndex:Float = waveformWidth / (endIndex - startIndex); + var indexesPerPixel:Float = (endIndex - startIndex) / waveformWidth; + + if (pixelsPerIndex >= 1.0) + { + // Each index is at least one pixel wide, so we render each index. + var prevVertexTopIndex:Int = -1; + var prevVertexBottomIndex:Int = -1; + for (i in startIndex...endIndex) + { + var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); + + var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleMapped(i) * waveformHeight / 2)); + var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleMapped(i) * waveformHeight / 2)); + + var vertexTopIndex:Int = meshRender.build_vertex(pixelPos, vertexTopY); + var vertexBottomIndex:Int = meshRender.build_vertex(pixelPos, vertexBottomY); + + if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + { + meshRender.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); + } + else + { + trace('Skipping quad at index ${i}'); + } + + prevVertexTopIndex = vertexTopIndex; + prevVertexBottomIndex = vertexBottomIndex; + } + } + else + { + // Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it. + var prevVertexTopIndex:Int = -1; + var prevVertexBottomIndex:Int = -1; + for (i in 0...waveformWidth) + { + // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. + var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); + var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); + + var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * waveformHeight / 2)); + var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * waveformHeight / 2)); + + // trace('Drawing index ${rangeStart} at pixel ${i} with MAX ${vertexTopY} and MIN ${vertexBottomY}'); + + var vertexTopIndex:Int = meshRender.build_vertex(i, vertexTopY); + var vertexBottomIndex:Int = meshRender.build_vertex(i, vertexBottomY); + + if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + { + meshRender.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); + } + else + { + trace('Skipping quad at index ${i}'); + } + + prevVertexTopIndex = vertexTopIndex; + prevVertexBottomIndex = vertexBottomIndex; + } + } + + trace('Drawing ${duration} seconds of waveform with ${meshRender.vertex_count} vertices'); + + var oneSecondInPixels:Float = waveformWidth / duration; + + timeMarker.x = Std.int(oneSecondInPixels); + + // For each sample in the waveform... + // Add a MAX vertex and a MIN vertex. + // If previous MAX/MIN is empty, store. + // If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN. + // Continue until end of waveform. + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.keys.justPressed.SPACE) + { + if (waveformAudio.isPlaying) + { + waveformAudio.stop(); + } + else + { + waveformAudio.play(); + } + } + + if (waveformAudio.isPlaying) + { + var songTimeSeconds:Float = waveformAudio.time / 1000; + drawWaveform(songTimeSeconds, duration); + } + + if (FlxG.keys.justPressed.UP) + { + trace('Zooming out'); + duration += 1.0; + drawTheWaveform(); + } + if (FlxG.keys.justPressed.DOWN) + { + trace('Zooming in'); + duration -= 1.0; + drawTheWaveform(); + } + if (FlxG.keys.justPressed.LEFT) + { + trace('Seeking back'); + time -= 1.0; + drawTheWaveform(); + } + if (FlxG.keys.justPressed.RIGHT) + { + trace('Seeking forward'); + time += 1.0; + drawTheWaveform(); + } + } + + var time:Float = 0.0; + var duration:Float = 5.0; + + function drawTheWaveform():Void + { + drawWaveform(time, duration); + } + + public override function destroy():Void + { + super.destroy(); + } +} From e8fa7f9c70be65638f876ba5b566b1d85a2f7555 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 23 Jan 2024 22:47:27 -0500 Subject: [PATCH 04/39] Performant audio waveforms generated directly from provided FlxSound elements. --- source/funkin/audio/waveform/WaveformData.hx | 95 +++++-- .../audio/waveform/WaveformDataParser.hx | 108 ++++++++ .../funkin/audio/waveform/WaveformSprite.hx | 244 ++++++++++++++++++ source/funkin/ui/debug/WaveformTestState.hx | 169 +++--------- 4 files changed, 473 insertions(+), 143 deletions(-) create mode 100644 source/funkin/audio/waveform/WaveformSprite.hx diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx index 525d1bd5fb..11d3ff641b 100644 --- a/source/funkin/audio/waveform/WaveformData.hx +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -5,6 +5,8 @@ import funkin.util.MathUtil; @:nullSafety class WaveformData { + static final DEFAULT_VERSION:Int = 2; + /** * The version of the waveform data format. * @default `2` (-1 if not specified/invalid) @@ -25,7 +27,7 @@ class WaveformData * Lower values can more accurately represent the waveform when zoomed in, but take more data. */ @:alias('samples_per_pixel') - public var samplesPerPixel(default, null):Int = 256; + public var samplesPerPoint(default, null):Int = 256; /** * Number of bits to use for each sample value. Valid values are `8` and `16`. @@ -33,9 +35,9 @@ class WaveformData public var bits(default, null):Int = 16; /** - * Number of output waveform data points. + * The length of the data array, in points. */ - public var length(default, null):Int = 0; // Array size is (4 * length) + public var length(default, null):Int = 0; /** * Array of Int16 values representing the waveform. @@ -46,7 +48,16 @@ class WaveformData @:jignored var channelData:Null> = null; - public function new() {} + public function new(?version:Int, channels:Int, sampleRate:Int, samplesPerPoint:Int, bits:Int, length:Int, data:Array) + { + this.version = version ?? DEFAULT_VERSION; + this.channels = channels; + this.sampleRate = sampleRate; + this.samplesPerPoint = samplesPerPoint; + this.bits = bits; + this.length = length; + this.data = data; + } function buildChannelData():Array { @@ -79,7 +90,7 @@ class WaveformData */ public function maxSampleValue():Int { - if (_maxSampleValue != -1) return _maxSampleValue; + if (_maxSampleValue != 0) return _maxSampleValue; return _maxSampleValue = Std.int(Math.pow(2, bits)); } @@ -87,14 +98,14 @@ class WaveformData * Cache the value because `Math.pow` is expensive and the value gets used a lot. */ @:jignored - var _maxSampleValue:Int = -1; + var _maxSampleValue:Int = 0; /** * @return The length of the waveform in samples. */ public function lenSamples():Int { - return length * samplesPerPixel; + return length * samplesPerPoint; } /** @@ -110,7 +121,7 @@ class WaveformData */ public function secondsToIndex(seconds:Float):Int { - return Std.int(seconds * sampleRate / samplesPerPixel); + return Std.int(seconds * pointsPerSecond()); } /** @@ -118,7 +129,15 @@ class WaveformData */ public function indexToSeconds(index:Int):Float { - return index * samplesPerPixel / sampleRate; + return index / pointsPerSecond(); + } + + /** + * The number of data points this waveform data provides per second of audio. + */ + public inline function pointsPerSecond():Float + { + return sampleRate / samplesPerPoint; } /** @@ -136,8 +155,50 @@ class WaveformData { return index / length; } + + /** + * Resample the waveform data to create a new WaveformData object matching the desired `samplesPerPoint` value. + * This is useful for zooming in/out of the waveform in a performant manner. + * + * @param newSamplesPerPoint The new value for `samplesPerPoint`. + */ + public function resample(newSamplesPerPoint:Int):WaveformData + { + var result = this.clone(); + + var ratio = newSamplesPerPoint / samplesPerPoint; + if (ratio == 1) return result; + if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.'); + + var inputSampleCount = this.lenSamples(); + var outputSampleCount = Std.int(inputSampleCount * ratio); + + var inputPointCount = this.length; + var outputPointCount = Std.int(inputPointCount / ratio); + var outputChannelCount = this.channels; + + // TODO: Actually figure out the dumbass logic for this. + + return result; + } + + /** + * Create a new WaveformData whose parameters match the current object. + */ + public function clone(?newData:Array = null):WaveformData + { + if (newData == null) + { + newData = this.data.clone(); + } + + var clone = new WaveformData(this.version, this.channels, this.sampleRate, this.samplesPerPoint, this.bits, newData.length, newData); + + return clone; + } } +@:nullSafety class WaveformDataChannel { var parent:WaveformData; @@ -149,6 +210,9 @@ class WaveformDataChannel this.channelId = channelId; } + /** + * Retrieve a given minimum point at an index. + */ public function minSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2; @@ -165,7 +229,7 @@ class WaveformDataChannel /** * Minimum value within the range of samples. - * @param i + * NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead. */ public function minSampleRange(start:Int, end:Int) { @@ -180,13 +244,15 @@ class WaveformDataChannel /** * Maximum value within the range of samples, mapped to a value between 0 and 1. - * @param i */ public function minSampleRangeMapped(start:Int, end:Int) { return minSampleRange(start, end) / parent.maxSampleValue(); } + /** + * Retrieve a given maximum point at an index. + */ public function maxSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2 + 1; @@ -203,7 +269,7 @@ class WaveformDataChannel /** * Maximum value within the range of samples. - * @param i + * NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead. */ public function maxSampleRange(start:Int, end:Int) { @@ -218,17 +284,12 @@ class WaveformDataChannel /** * Maximum value within the range of samples, mapped to a value between 0 and 1. - * @param i */ public function maxSampleRangeMapped(start:Int, end:Int) { return maxSampleRange(start, end) / parent.maxSampleValue(); } - /** - * Maximum value within the range of samples, mapped to a value between 0 and 1. - * @param i - */ public function setMinSample(i:Int, value:Int) { var offset = (i * parent.channels + this.channelId) * 2; diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx index fb453cf1b6..2e5c52d13e 100644 --- a/source/funkin/audio/waveform/WaveformDataParser.hx +++ b/source/funkin/audio/waveform/WaveformDataParser.hx @@ -2,6 +2,113 @@ package funkin.audio.waveform; class WaveformDataParser { + static final INT16_MAX:Int = 32767; + static final INT16_MIN:Int = -32768; + + static final INT8_MAX:Int = 127; + static final INT8_MIN:Int = -128; + + public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null + { + if (sound == null) return null; + + // Method 1. This only works if the sound has been played before. + @:privateAccess + var soundBuffer:Null = sound?._channel?.__source?.buffer; + + if (soundBuffer == null) + { + // Method 2. This works if the sound has not been played before. + @:privateAccess + soundBuffer = sound?._sound?.__buffer; + + if (soundBuffer == null) + { + trace('[WAVEFORM] Failed to interpret FlxSound: ${sound}'); + return null; + } + else + { + trace('[WAVEFORM] Method 2 worked.'); + } + } + else + { + trace('[WAVEFORM] Method 1 worked.'); + } + + return interpretAudioBuffer(soundBuffer); + } + + public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null + { + var sampleRate = soundBuffer.sampleRate; + var channels = soundBuffer.channels; + var bitsPerSample = soundBuffer.bitsPerSample; + var samplesPerPoint:Int = 256; // I don't think we need to configure this. + var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant.. + + // TODO: Make this work better on HTML5. + var soundData:lime.utils.Int16Array = cast soundBuffer.data; + + var soundDataRawLength:Int = soundData.length; + 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}'); + + var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN; + var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX; + + var outputData:Array = []; + + for (pointIndex in 0...outputPointCount) + { + // minChannel1, maxChannel1, minChannel2, maxChannel2, ... + var values:Array = []; + + for (i in 0...channels) + { + values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX); + values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN); + } + + var rangeStart = pointIndex * samplesPerPoint; + var rangeEnd = rangeStart + samplesPerPoint; + if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount; + + for (sampleIndex in rangeStart...rangeEnd) + { + for (channelIndex in 0...channels) + { + var sampleIndex:Int = sampleIndex * channels + channelIndex; + var sampleValue = soundData[sampleIndex]; + + if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue; + if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue; + } + } + + // We now have the min and max values for the range. + for (value in values) + outputData.push(value); + } + + var outputDataLength:Int = Std.int(outputData.length / channels / 2); + var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData); + + return result; + } + public static function parseWaveformData(path:String):Null { var rawJson:String = openfl.Assets.getText(path).trim(); @@ -12,6 +119,7 @@ class WaveformDataParser { var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = false; + trace('[WAVEFORM] Parsing waveform data: ${contents}'); parser.fromJson(contents, fileName); if (parser.errors.length > 0) diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx new file mode 100644 index 0000000000..46a8352d97 --- /dev/null +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -0,0 +1,244 @@ +package funkin.audio.waveform; + +import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformDataParser; +import funkin.graphics.rendering.MeshRender; +import flixel.util.FlxColor; + +class WaveformSprite extends MeshRender +{ + static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE; + static final DEFAULT_DURATION:Float = 5.0; + static final DEFAULT_ORIENTATION:WaveformOrientation = HORIZONTAL; + static final DEFAULT_X:Float = 0.0; + static final DEFAULT_Y:Float = 0.0; + static final DEFAULT_WIDTH:Float = 100.0; + static final DEFAULT_HEIGHT:Float = 100.0; + + /** + * Set this to true to tell the waveform to rebuild itself. + * Do this any time the data or drawable area of the waveform changes. + * This often (but not always) needs to be done every frame. + */ + var isWaveformDirty:Bool = true; + + public var waveformData:WaveformData; + + function set_waveformData(value:WaveformData):WaveformData + { + waveformData = value; + isWaveformDirty = true; + return waveformData; + } + + /** + * The color to render the waveform with. + */ + public var waveformColor(default, set):FlxColor; + + function set_waveformColor(value:FlxColor):FlxColor + { + waveformColor = value; + // We don't need to dirty the waveform geometry, just rebuild the texture. + rebuildGraphic(); + return waveformColor; + } + + public var orientation(default, set):WaveformOrientation; + + function set_orientation(value:WaveformOrientation):WaveformOrientation + { + orientation = value; + isWaveformDirty = true; + return orientation; + } + + /** + * Time, in seconds, at which the waveform starts. + */ + public var time(default, set):Float; + + function set_time(value:Float) + { + time = value; + isWaveformDirty = true; + return time; + } + + /** + * The duration, in seconds, that the waveform represents. + * The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents. + */ + public var duration(default, set):Float; + + function set_duration(value:Float) + { + duration = value; + isWaveformDirty = true; + return duration; + } + + /** + * Set the physical size of the waveform with `this.height = value`. + */ + override function set_height(value:Float):Float + { + isWaveformDirty = true; + return super.set_height(value); + } + + /** + * Set the physical size of the waveform with `this.width = value`. + */ + override function set_width(value:Float):Float + { + isWaveformDirty = true; + return super.set_width(value); + } + + public function new(waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + { + super(DEFAULT_X, DEFAULT_Y, DEFAULT_COLOR); + this.waveformColor = color ?? DEFAULT_COLOR; + this.width = DEFAULT_WIDTH; + this.height = DEFAULT_HEIGHT; + + this.waveformData = waveformData; + this.orientation = orientation; + this.time = 0.0; + this.duration = duration; + } + + public override function update(elapsed:Float) + { + super.update(elapsed); + + if (isWaveformDirty) + { + // Recalculate the waveform vertices. + drawWaveform(); + isWaveformDirty = false; + } + } + + function rebuildGraphic():Void + { + // The waveform is rendered using a single colored pixel as a texture. + // If you want something more elaborate, make sure to modify `build_vertex` below to use the UVs you want. + makeGraphic(1, 1, this.waveformColor); + } + + /** + * @param offsetX Horizontal offset to draw the waveform at, in samples. + */ + function drawWaveform():Void + { + // For each sample in the waveform... + // Add a MAX vertex and a MIN vertex. + // If previous MAX/MIN is empty, store. + // If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN. + // Continue until end of waveform. + + this.clear(); + + // Center point of the waveform. When horizontal this is half the height, when vertical this is half the width. + var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2); + + var oneSecondInIndices:Int = waveformData.secondsToIndex(1); + + var startTime:Float = time; + var endTime:Float = time + duration; + + var startIndex:Int = waveformData.secondsToIndex(startTime); + var endIndex:Int = waveformData.secondsToIndex(endTime); + + var pixelsPerIndex:Float = (orientation == HORIZONTAL ? this.width : this.height) / (endIndex - startIndex); + var indexesPerPixel:Float = 1 / pixelsPerIndex; + + if (pixelsPerIndex >= 1.0) + { + // Each index is at least one pixel wide, so we render each index. + var prevVertexTopIndex:Int = -1; + var prevVertexBottomIndex:Int = -1; + for (i in startIndex...endIndex) + { + var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); + + var vertexTopY:Int = Std.int(waveformCenterPos + - (waveformData.channel(0).maxSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + var vertexBottomY:Int = Std.int(waveformCenterPos + + (-waveformData.channel(0).minSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + + var vertexTopIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(pixelPos, vertexTopY) : this.build_vertex(vertexTopY, pixelPos); + var vertexBottomIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(pixelPos, vertexBottomY) : this.build_vertex(vertexBottomY, pixelPos); + + if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + { + switch (orientation) // the line of code that makes you gay + { + case HORIZONTAL: + this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); + case VERTICAL: + this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex); + } + } + + prevVertexTopIndex = vertexTopIndex; + prevVertexBottomIndex = vertexBottomIndex; + } + } + else + { + // Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it. + var prevVertexTopIndex:Int = -1; + var prevVertexBottomIndex:Int = -1; + var waveformLengthPixels:Int = orientation == HORIZONTAL ? Std.int(this.width) : Std.int(this.height); + for (i in 0...waveformLengthPixels) + { + // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. + var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); + var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); + + var vertexTopY:Int = Std.int(waveformCenterPos + - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + var vertexBottomY:Int = Std.int(waveformCenterPos + + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + + var vertexTopIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(i, vertexTopY) : this.build_vertex(vertexTopY, i); + var vertexBottomIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(i, vertexBottomY) : this.build_vertex(vertexBottomY, i); + + if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + { + switch (orientation) + { + case HORIZONTAL: + this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); + case VERTICAL: + this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex); + } + } + prevVertexTopIndex = vertexTopIndex; + prevVertexBottomIndex = vertexBottomIndex; + } + } + } + + public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + { + return new WaveformSprite(data, orientation, duration); + } + + public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + { + // TODO: Build waveform data from FunkinSound. + var data = null; + + return buildFromWaveformData(data, orientation, color, duration); + } +} + +enum WaveformOrientation +{ + HORIZONTAL; + VERTICAL; +} diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx index b202116b95..783179c008 100644 --- a/source/funkin/ui/debug/WaveformTestState.hx +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -4,6 +4,7 @@ import flixel.FlxSprite; import flixel.util.FlxColor; import funkin.audio.FunkinSound; import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformSprite; import funkin.audio.waveform.WaveformDataParser; import funkin.graphics.rendering.MeshRender; @@ -15,186 +16,102 @@ class WaveformTestState extends MusicBeatState } var waveformData:WaveformData; + var waveformData2:WaveformData; var waveformAudio:FunkinSound; - var meshRender:MeshRender; + var waveformSprite:WaveformSprite; + // var waveformSprite2:WaveformSprite; var timeMarker:FlxSprite; public override function create():Void { super.create(); - waveformData = WaveformDataParser.parseWaveformData(Paths.json("waveform/dadbattle-erect/dadbattle-erect.waveform")); + waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect')); - waveformAudio = FunkinSound.load(Paths.music('dadbattle-erect/dadbattle-erect')); + // waveformData = WaveformDataParser.parseWaveformData(Paths.json('waveform/dadbattle-erect/dadbattle-erect.waveform')); + waveformData = WaveformDataParser.interpretFlxSound(waveformAudio); - var lightBlue:FlxColor = FlxColor.fromString("#ADD8E6"); - meshRender = new MeshRender(0, 0, lightBlue); - add(meshRender); + waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"), 5.0); + waveformSprite.width = FlxG.width; + waveformSprite.height = FlxG.height; // / 2; + add(waveformSprite); + + // waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0); + // waveformSprite2.width = FlxG.width; + // waveformSprite2.height = FlxG.height / 2; + // waveformSprite2.y = FlxG.height / 2; + // add(waveformSprite2); timeMarker = new FlxSprite(0, FlxG.height * 1 / 6); timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED); add(timeMarker); - drawWaveform(time, duration); + // drawWaveform(time, duration); } - /** - * @param offsetX Horizontal offset to draw the waveform at, in samples. - */ - function drawWaveform(timeSeconds:Float, duration:Float):Void + public override function update(elapsed:Float):Void { - meshRender.clear(); - - var offsetX:Int = waveformData.secondsToIndex(timeSeconds); - - var waveformHeight:Int = Std.int(FlxG.height * (2 / 3)); - var waveformWidth:Int = FlxG.width; - var waveformCenterPos:Int = Std.int(FlxG.height / 2); - - var oneSecondInIndices:Int = waveformData.secondsToIndex(1); - - var startTime:Float = -1.0; - var endTime:Float = startTime + duration; - - var startIndex:Int = Std.int(offsetX + (oneSecondInIndices * startTime)); - var endIndex:Int = Std.int(offsetX + (oneSecondInIndices * (startTime + duration))); - - var pixelsPerIndex:Float = waveformWidth / (endIndex - startIndex); - var indexesPerPixel:Float = (endIndex - startIndex) / waveformWidth; + super.update(elapsed); - if (pixelsPerIndex >= 1.0) + if (FlxG.keys.justPressed.SPACE) { - // Each index is at least one pixel wide, so we render each index. - var prevVertexTopIndex:Int = -1; - var prevVertexBottomIndex:Int = -1; - for (i in startIndex...endIndex) + if (waveformAudio.isPlaying) { - var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); - - var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleMapped(i) * waveformHeight / 2)); - var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleMapped(i) * waveformHeight / 2)); - - var vertexTopIndex:Int = meshRender.build_vertex(pixelPos, vertexTopY); - var vertexBottomIndex:Int = meshRender.build_vertex(pixelPos, vertexBottomY); - - if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) - { - meshRender.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); - } - else - { - trace('Skipping quad at index ${i}'); - } - - prevVertexTopIndex = vertexTopIndex; - prevVertexBottomIndex = vertexBottomIndex; + waveformAudio.stop(); } - } - else - { - // Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it. - var prevVertexTopIndex:Int = -1; - var prevVertexBottomIndex:Int = -1; - for (i in 0...waveformWidth) + else { - // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. - var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); - var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); - - var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * waveformHeight / 2)); - var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * waveformHeight / 2)); - - // trace('Drawing index ${rangeStart} at pixel ${i} with MAX ${vertexTopY} and MIN ${vertexBottomY}'); - - var vertexTopIndex:Int = meshRender.build_vertex(i, vertexTopY); - var vertexBottomIndex:Int = meshRender.build_vertex(i, vertexBottomY); - - if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) - { - meshRender.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); - } - else - { - trace('Skipping quad at index ${i}'); - } - - prevVertexTopIndex = vertexTopIndex; - prevVertexBottomIndex = vertexBottomIndex; + waveformAudio.play(); } } - trace('Drawing ${duration} seconds of waveform with ${meshRender.vertex_count} vertices'); - - var oneSecondInPixels:Float = waveformWidth / duration; - - timeMarker.x = Std.int(oneSecondInPixels); - - // For each sample in the waveform... - // Add a MAX vertex and a MIN vertex. - // If previous MAX/MIN is empty, store. - // If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN. - // Continue until end of waveform. - } - - public override function update(elapsed:Float):Void - { - super.update(elapsed); - - if (FlxG.keys.justPressed.SPACE) + if (FlxG.keys.justPressed.ENTER) { - if (waveformAudio.isPlaying) + if (waveformSprite.orientation == HORIZONTAL) { - waveformAudio.stop(); + waveformSprite.orientation = VERTICAL; + // waveformSprite2.orientation = VERTICAL; } else { - waveformAudio.play(); + waveformSprite.orientation = HORIZONTAL; + // waveformSprite2.orientation = HORIZONTAL; } } if (waveformAudio.isPlaying) { - var songTimeSeconds:Float = waveformAudio.time / 1000; - drawWaveform(songTimeSeconds, duration); + // waveformSprite takes a time in fractional seconds, not milliseconds. + var timeSeconds = waveformAudio.time / 1000; + waveformSprite.time = timeSeconds; + // waveformSprite2.time = timeSeconds; } if (FlxG.keys.justPressed.UP) { - trace('Zooming out'); - duration += 1.0; - drawTheWaveform(); + waveformSprite.duration += 1.0; + // waveformSprite2.duration += 1.0; } if (FlxG.keys.justPressed.DOWN) { - trace('Zooming in'); - duration -= 1.0; - drawTheWaveform(); + waveformSprite.duration -= 1.0; + // waveformSprite2.duration -= 1.0; } if (FlxG.keys.justPressed.LEFT) { - trace('Seeking back'); - time -= 1.0; - drawTheWaveform(); + waveformSprite.time -= 1.0; + // waveformSprite2.time -= 1.0; } if (FlxG.keys.justPressed.RIGHT) { - trace('Seeking forward'); - time += 1.0; - drawTheWaveform(); + waveformSprite.time += 1.0; + // waveformSprite2.time += 1.0; } } - var time:Float = 0.0; - var duration:Float = 5.0; - - function drawTheWaveform():Void - { - drawWaveform(time, duration); - } - public override function destroy():Void { super.destroy(); From a1cff6be509665e3f54bd254134a373131e6a76b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 23 Jan 2024 23:24:05 -0500 Subject: [PATCH 05/39] Replace waveform renderer with the newer, more performant version. --- .../ui/debug/charting/ChartEditorState.hx | 25 ++++----- .../handlers/ChartEditorAudioHandler.hx | 53 ++++++++++++++----- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index fbb1c9743d..b3c703287f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -23,6 +23,7 @@ import flixel.system.FlxAssets.FlxSoundAsset; 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; @@ -129,7 +130,6 @@ import haxe.ui.focus.FocusManager; import openfl.display.BitmapData; import funkin.audio.visualize.PolygonSpectogram; import flixel.group.FlxGroup.FlxTypedGroup; -import funkin.audio.visualize.PolygonVisGroup; import flixel.input.mouse.FlxMouseEvent; import flixel.text.FlxText; import flixel.system.debug.log.LogStyle; @@ -395,13 +395,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); measureTicks.y = gridTiledSprite.y; - if (audioVisGroup != null && audioVisGroup.playerVis != null) + for (member in audioWaveforms.members) { - audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS - GRID_TOP_PAD); - } - if (audioVisGroup != null && audioVisGroup.opponentVis != null) - { - audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS - GRID_TOP_PAD); + member.time = scrollPositionInMs / Constants.MS_PER_SEC; + + // Doing this desyncs the waveforms from the grid. + // member.y = Math.max(this.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); } } } @@ -503,8 +502,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function get_playheadPositionInMs():Float { - if (audioVisGroup != null && audioVisGroup.playerVis != null) - audioVisGroup.playerVis.realtimeStartOffset = -Conductor.instance.getStepTimeInMs(playheadPositionInSteps); return Conductor.instance.getStepTimeInMs(playheadPositionInSteps); } @@ -512,7 +509,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { playheadPositionInSteps = Conductor.instance.getTimeInSteps(value); - if (audioVisGroup != null && audioVisGroup.playerVis != null) audioVisGroup.playerVis.realtimeStartOffset = -value; return value; } @@ -1134,11 +1130,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var audioVocalTrackGroup:Null = null; /** - * The audio vis for the inst/vocals. + * The audio waveform visualization for the inst/vocals. * `null` until vocal track(s) are loaded. - * When switching characters, the elements of the PolygonVisGroup will be swapped to match the new character. + * When switching characters, the elements will be swapped to match the new character. */ - var audioVisGroup:Null = null; + var audioWaveforms:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(); /** * A map of the audio tracks for each character's vocals. @@ -2302,8 +2298,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(healthIconBF); healthIconBF.zIndex = 30; - audioVisGroup = new PolygonVisGroup(); - add(audioVisGroup); + add(audioWaveforms); } function buildMeasureTicks():Void diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index d7fbd42c2b..2bc0339a92 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -3,11 +3,14 @@ package funkin.ui.debug.charting.handlers; import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxSound; import funkin.audio.VoicesGroup; -import funkin.audio.visualize.PolygonVisGroup; import funkin.audio.FunkinSound; import funkin.play.character.BaseCharacter.CharacterType; import funkin.util.FileUtil; import funkin.util.assets.SoundUtil; +import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformDataParser; +import funkin.audio.waveform.WaveformSprite; +import flixel.util.FlxColor; import haxe.io.Bytes; import haxe.io.Path; import openfl.utils.Assets; @@ -175,7 +178,6 @@ class ChartEditorAudioHandler var vocalTrack:Null = SoundUtil.buildSoundFromBytes(vocalTrackData); if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup(); - if (state.audioVisGroup == null) state.audioVisGroup = new PolygonVisGroup(); if (vocalTrack != null) { @@ -183,24 +185,47 @@ class ChartEditorAudioHandler { case BF: state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); - state.audioVisGroup.addPlayerVis(vocalTrack); - state.audioVisGroup.playerVis.x = 885; - state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195; - state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; - state.audioVisGroup.playerVis.detail = 1; - state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); + + var waveformData:Null = WaveformDataParser.interpretFlxSound(vocalTrack); + if (waveformData != null) + { + var duration:Float = Conductor.instance.getStepTimeInMs(16) * 0.001; + var waveformSprite:WaveformSprite = new WaveformSprite(waveformData, VERTICAL, FlxColor.WHITE); + waveformSprite.x = 840; + waveformSprite.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); + waveformSprite.height = (ChartEditorState.GRID_SIZE) * 16; + waveformSprite.width = (ChartEditorState.GRID_SIZE) * 2; + waveformSprite.time = 0; + waveformSprite.duration = duration; + state.audioWaveforms.add(waveformSprite); + } + else + { + trace('[WARN] Failed to parse waveform data for vocal track.'); + } state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); return true; case DAD: state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); - state.audioVisGroup.addOpponentVis(vocalTrack); - state.audioVisGroup.opponentVis.x = 405; - state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195; - state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; - state.audioVisGroup.opponentVis.detail = 1; - state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); + var waveformData:Null = WaveformDataParser.interpretFlxSound(vocalTrack); + if (waveformData != null) + { + var duration:Float = Conductor.instance.getStepTimeInMs(16) * 0.001; + var waveformSprite:WaveformSprite = new WaveformSprite(waveformData, VERTICAL, FlxColor.WHITE); + waveformSprite.x = 360; + waveformSprite.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); + waveformSprite.height = (ChartEditorState.GRID_SIZE) * 16; + waveformSprite.width = (ChartEditorState.GRID_SIZE) * 2; + waveformSprite.time = 0; + waveformSprite.duration = duration; + state.audioWaveforms.add(waveformSprite); + } + else + { + trace('[WARN] Failed to parse waveform data for vocal track.'); + } state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); From ba674b1f92bdcc31c8ab0193fb018d6b0037d3b9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 24 Jan 2024 14:35:50 -0500 Subject: [PATCH 06/39] Added support for clipRects to WaveformSprite. --- .../funkin/audio/waveform/WaveformSprite.hx | 152 +++++++++++++++++- source/funkin/ui/debug/WaveformTestState.hx | 11 +- 2 files changed, 156 insertions(+), 7 deletions(-) diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index 46a8352d97..18c7915cd7 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -155,6 +155,19 @@ class WaveformSprite extends MeshRender var pixelsPerIndex:Float = (orientation == HORIZONTAL ? this.width : this.height) / (endIndex - startIndex); var indexesPerPixel:Float = 1 / pixelsPerIndex; + var topLeftVertexIndex:Int = -1; + var topRightVertexIndex:Int = -1; + var bottomLeftVertexIndex:Int = -1; + var bottomRightVertexIndex:Int = -1; + + if (clipRect != null) + { + topLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y); + topRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y); + bottomLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y + clipRect.height); + bottomRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y + clipRect.height); + } + if (pixelsPerIndex >= 1.0) { // Each index is at least one pixel wide, so we render each index. @@ -164,15 +177,55 @@ class WaveformSprite extends MeshRender { var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); + var isOutsideClipRectHorizontal:Bool = (clipRect != null) && (pixelPos < clipRect.x || pixelPos > (clipRect.x + clipRect.width)); + var isOutsideClipRectVertical:Bool = (clipRect != null) && (pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height)); + var isOutsideClipRect:Bool = orientation == HORIZONTAL ? isOutsideClipRectHorizontal : isOutsideClipRectVertical; + + // This index is outside the clipRect, so we can just skip rendering it. Fantastic! + if (isOutsideClipRect) continue; + var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); - var vertexTopIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(pixelPos, vertexTopY) : this.build_vertex(vertexTopY, pixelPos); - var vertexBottomIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(pixelPos, vertexBottomY) : this.build_vertex(vertexBottomY, pixelPos); + var vertexTopIndex:Int = -1; + var vertexBottomIndex:Int = -1; - if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + if (clipRect != null) + { + if (orientation == HORIZONTAL) + { + vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex); + vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, + bottomRightVertexIndex); + } + else + { + vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex); + vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, + bottomRightVertexIndex); + } + } + else + { + if (orientation == HORIZONTAL) + { + vertexTopIndex = this.build_vertex(pixelPos, vertexTopY); + vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY); + } + else + { + vertexTopIndex = this.build_vertex(vertexTopY, pixelPos); + vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos); + } + } + + // Don't render if we don't have a previous different set of vertices to create a quad from. + if (prevVertexTopIndex != -1 + && prevVertexBottomIndex != -1 + && prevVertexTopIndex != vertexTopIndex + && prevVertexBottomIndex != vertexBottomIndex) { switch (orientation) // the line of code that makes you gay { @@ -198,14 +251,51 @@ class WaveformSprite extends MeshRender // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); + var pixelPos:Int = i; + + var isOutsideClipRectHorizontal:Bool = (clipRect != null) && (pixelPos < clipRect.x || pixelPos > (clipRect.x + clipRect.width)); + var isOutsideClipRectVertical:Bool = (clipRect != null) && (pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height)); + var isOutsideClipRect:Bool = orientation == HORIZONTAL ? isOutsideClipRectHorizontal : isOutsideClipRectVertical; + + // This index is outside the clipRect, so we can just skip rendering it. Fantastic! + if (isOutsideClipRect) continue; var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); - var vertexTopIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(i, vertexTopY) : this.build_vertex(vertexTopY, i); - var vertexBottomIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(i, vertexBottomY) : this.build_vertex(vertexBottomY, i); + var vertexTopIndex:Int = -1; + var vertexBottomIndex:Int = -1; + + if (clipRect != null) + { + if (orientation == HORIZONTAL) + { + vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex); + vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, + bottomRightVertexIndex); + } + else + { + vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex); + vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, + bottomRightVertexIndex); + } + } + else + { + if (orientation == HORIZONTAL) + { + vertexTopIndex = this.build_vertex(pixelPos, vertexTopY); + vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY); + } + else + { + vertexTopIndex = this.build_vertex(vertexTopY, pixelPos); + vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos); + } + } if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) { @@ -221,11 +311,61 @@ class WaveformSprite extends MeshRender prevVertexBottomIndex = vertexBottomIndex; } } + + trace('Rendering waveform of ${duration} seconds with ${this.vertex_count} vertices.'); + } + + function buildClippedVertex(x:Int, y:Int, topLeftVertexIndex:Int, topRightVertexIndex:Int, bottomLeftVertexIndex:Int, bottomRightVertexIndex:Int):Int + { + var shouldClipXLeft = x < clipRect.x; + var shouldClipXRight = x > (clipRect.x + clipRect.width); + var shouldClipYTop = y < clipRect.y; + var shouldClipYBottom = y > (clipRect.y + clipRect.height); + + // If the vertex is fully outside the clipRect, use a pre-existing vertex. + // Else, if the vertex is outside the clipRect on one axis, create a new vertex constrained on that axis. + // Else, create a whole new vertex. + if (shouldClipXLeft && shouldClipYTop) + { + return topLeftVertexIndex; + } + else if (shouldClipXRight && shouldClipYTop) + { + return topRightVertexIndex; + } + else if (shouldClipXLeft && shouldClipYBottom) + { + return bottomLeftVertexIndex; + } + else if (shouldClipXRight && shouldClipYBottom) + { + return bottomRightVertexIndex; + } + else if (shouldClipXLeft) + { + return this.build_vertex(clipRect.x, y); + } + else if (shouldClipXRight) + { + return this.build_vertex(clipRect.x + clipRect.width, y); + } + else if (shouldClipYTop) + { + return this.build_vertex(x, clipRect.y); + } + else if (shouldClipYBottom) + { + return this.build_vertex(x, clipRect.y + clipRect.height); + } + else + { + return this.build_vertex(x, y); + } } public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) { - return new WaveformSprite(data, orientation, duration); + return new WaveformSprite(data, orientation, color, duration); } public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx index 783179c008..354adf0bf7 100644 --- a/source/funkin/ui/debug/WaveformTestState.hx +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -1,5 +1,6 @@ package funkin.ui.debug; +import flixel.math.FlxRect; import flixel.FlxSprite; import flixel.util.FlxColor; import funkin.audio.FunkinSound; @@ -29,14 +30,22 @@ class WaveformTestState extends MusicBeatState { super.create(); + var testSprite = new FlxSprite(0, 0); + testSprite.loadGraphic(Paths.image('funkay')); + testSprite.updateHitbox(); + testSprite.clipRect = new FlxRect(0, 0, FlxG.width * 9 / 16, FlxG.height); + add(testSprite); + waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect')); // waveformData = WaveformDataParser.parseWaveformData(Paths.json('waveform/dadbattle-erect/dadbattle-erect.waveform')); waveformData = WaveformDataParser.interpretFlxSound(waveformAudio); - waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"), 5.0); + waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"), 2.5); waveformSprite.width = FlxG.width; waveformSprite.height = FlxG.height; // / 2; + waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width * 9 / 16, FlxG.height); + // waveformSprite.clipRect = new FlxRect(FlxG.width * 1 / 3, FlxG.height * 3 / 8, FlxG.width * 1 / 3, FlxG.height * 2 / 8); add(waveformSprite); // waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0); From 17bf3289b5924123ef1e06d354e465fdde095a1f Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 24 Jan 2024 14:54:29 -0500 Subject: [PATCH 07/39] Fix for default constructor values. --- source/funkin/audio/waveform/WaveformSprite.hx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index 18c7915cd7..228345134e 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -14,6 +14,7 @@ class WaveformSprite extends MeshRender static final DEFAULT_Y:Float = 0.0; static final DEFAULT_WIDTH:Float = 100.0; static final DEFAULT_HEIGHT:Float = 100.0; + static final DEFAULT_DURATION:Float = 5.0; /** * Set this to true to tell the waveform to rebuild itself. @@ -104,9 +105,9 @@ class WaveformSprite extends MeshRender this.height = DEFAULT_HEIGHT; this.waveformData = waveformData; - this.orientation = orientation; + this.orientation = orientation ?? DEFAULT_ORIENTATION; this.time = 0.0; - this.duration = duration; + this.duration = duration ?? DEFAULT_DURATION; } public override function update(elapsed:Float) From abebfd8300f95560cb7cf88a72733e27098aa22c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 24 Jan 2024 15:47:43 -0500 Subject: [PATCH 08/39] Added a forceUpdate option, and improved efficiency of other setters. --- .../funkin/audio/waveform/WaveformSprite.hx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index 228345134e..242ed0167c 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -23,10 +23,18 @@ class WaveformSprite extends MeshRender */ var isWaveformDirty:Bool = true; + /** + * If true, force the waveform to redraw every frame. + * Useful if the waveform's clipRect is constantly changing. + */ + public var forceUpdate:Bool = false; + public var waveformData:WaveformData; function set_waveformData(value:WaveformData):WaveformData { + if (waveformData == value) return value; + waveformData = value; isWaveformDirty = true; return waveformData; @@ -39,6 +47,8 @@ class WaveformSprite extends MeshRender function set_waveformColor(value:FlxColor):FlxColor { + if (waveformColor == value) return value; + waveformColor = value; // We don't need to dirty the waveform geometry, just rebuild the texture. rebuildGraphic(); @@ -49,6 +59,8 @@ class WaveformSprite extends MeshRender function set_orientation(value:WaveformOrientation):WaveformOrientation { + if (orientation == value) return value; + orientation = value; isWaveformDirty = true; return orientation; @@ -61,6 +73,8 @@ class WaveformSprite extends MeshRender function set_time(value:Float) { + if (time == value) return value; + time = value; isWaveformDirty = true; return time; @@ -74,6 +88,8 @@ class WaveformSprite extends MeshRender function set_duration(value:Float) { + if (duration == value) return value; + duration = value; isWaveformDirty = true; return duration; @@ -84,6 +100,8 @@ class WaveformSprite extends MeshRender */ override function set_height(value:Float):Float { + if (height == value) return super.set_height(value); + isWaveformDirty = true; return super.set_height(value); } @@ -93,6 +111,8 @@ class WaveformSprite extends MeshRender */ override function set_width(value:Float):Float { + if (width == value) return super.set_width(value); + isWaveformDirty = true; return super.set_width(value); } @@ -108,13 +128,15 @@ class WaveformSprite extends MeshRender this.orientation = orientation ?? DEFAULT_ORIENTATION; this.time = 0.0; this.duration = duration ?? DEFAULT_DURATION; + + this.forceUpdate = false; } public override function update(elapsed:Float) { super.update(elapsed); - if (isWaveformDirty) + if (forceUpdate || isWaveformDirty) { // Recalculate the waveform vertices. drawWaveform(); From e111e91a9c9f7acbbcda9fe9008eb0a9c8d47de6 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Wed, 24 Jan 2024 20:31:25 -0700 Subject: [PATCH 09/39] Editor: Added ability to set units for event properties. Fixed bug where event icon tooltip wouldn't show an enum text value correctly. --- source/funkin/data/event/SongEventSchema.hx | 31 +++++++++++++++---- .../funkin/play/event/FocusCameraSongEvent.hx | 6 ++-- .../toolboxes/ChartEditorEventDataToolbox.hx | 10 ++++++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/source/funkin/data/event/SongEventSchema.hx b/source/funkin/data/event/SongEventSchema.hx index 7ebaa5ae1d..c910cbf5a0 100644 --- a/source/funkin/data/event/SongEventSchema.hx +++ b/source/funkin/data/event/SongEventSchema.hx @@ -6,7 +6,7 @@ import funkin.data.song.SongData.SongEventData; import funkin.util.macro.ClassMacro; import funkin.play.event.ScriptedSongEvent; -@:forward(name, tittlte, type, keys, min, max, step, defaultValue, iterator) +@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator) abstract SongEventSchema(SongEventSchemaRaw) { public function new(?fields:Array) @@ -42,7 +42,7 @@ abstract SongEventSchema(SongEventSchemaRaw) return this[k] = v; } - public function stringifyFieldValue(name:String, value:Dynamic):String + public function stringifyFieldValue(name:String, value:Dynamic, addUnits:Bool = true):String { var field:SongEventSchemaField = getByName(name); if (field == null) return 'Unknown'; @@ -52,21 +52,34 @@ abstract SongEventSchema(SongEventSchemaRaw) case SongEventFieldType.STRING: return Std.string(value); case SongEventFieldType.INTEGER: - return Std.string(value); + var returnValue:String = Std.string(value); + if (addUnits) return addUnitsToString(returnValue, field); + return returnValue; case SongEventFieldType.FLOAT: - return Std.string(value); + var returnValue:String = Std.string(value); + if (addUnits) return addUnitsToString(returnValue, field); + return returnValue; case SongEventFieldType.BOOL: return Std.string(value); case SongEventFieldType.ENUM: + var valueString:String = Std.string(value); for (key in field.keys.keys()) { - if (field.keys.get(key) == value) return key; + // Comparing these values as strings because comparing Dynamic variables is jank. + if (Std.string(field.keys.get(key)) == valueString) return key; } - return Std.string(value); + return valueString; default: return 'Unknown'; } } + + function addUnitsToString(value:String, field:SongEventSchemaField) + { + if (field.units == null || field.units == '') return value; + + return value + ' ${field.units}'; + } } typedef SongEventSchemaRaw = Array; @@ -115,6 +128,12 @@ typedef SongEventSchemaField = */ ?step:Float, + /** + * Used for INTEGER and FLOAT values. + * The units that the value is expressed in (pixels, percent, etc). + */ + ?units:String, + /** * An optional default value for the field. */ diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 83c978ba81..034d798947 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -135,10 +135,10 @@ class FocusCameraSongEvent extends SongEvent return new SongEventSchema([ { name: "char", - title: "Character", + title: "Target", defaultValue: 0, type: SongEventFieldType.ENUM, - keys: ["Position" => -1, "Boyfriend" => 0, "Dad" => 1, "Girlfriend" => 2] + keys: ["Position" => -1, "Player" => 0, "Opponent" => 1, "Girlfriend" => 2] }, { name: "x", @@ -146,6 +146,7 @@ class FocusCameraSongEvent extends SongEvent defaultValue: 0, step: 10.0, type: SongEventFieldType.FLOAT, + units: "px", }, { name: "y", @@ -153,6 +154,7 @@ class FocusCameraSongEvent extends SongEvent defaultValue: 0, step: 10.0, type: SongEventFieldType.FLOAT, + units: "px", } ]); } diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx index fbd1562b4d..9ae4cbb013 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -216,6 +216,16 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox target.addComponent(input); + if (field.units != null && field.units != "") + { + var units:Label = new Label(); + units.text = field.units; + units.verticalAlign = "center"; + units.left = 85; + units.top = 4; + input.addComponent(units); + } + // Update the value of the event data. input.onChange = function(event:UIEvent) { var value = event.target.value; From 25aff02784ddb46674f243f71066992442b8e2bc Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 25 Jan 2024 19:10:33 -0500 Subject: [PATCH 10/39] Added options for waveform amplitude and minimum size. --- .../funkin/audio/waveform/WaveformSprite.hx | 79 +++++++++++++------ source/funkin/ui/debug/WaveformTestState.hx | 13 +-- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index 242ed0167c..99da306140 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -14,7 +14,6 @@ class WaveformSprite extends MeshRender static final DEFAULT_Y:Float = 0.0; static final DEFAULT_WIDTH:Float = 100.0; static final DEFAULT_HEIGHT:Float = 100.0; - static final DEFAULT_DURATION:Float = 5.0; /** * Set this to true to tell the waveform to rebuild itself. @@ -29,9 +28,9 @@ class WaveformSprite extends MeshRender */ public var forceUpdate:Bool = false; - public var waveformData:WaveformData; + public var waveformData(default, set):Null; - function set_waveformData(value:WaveformData):WaveformData + function set_waveformData(value:Null):Null { if (waveformData == value) return value; @@ -117,7 +116,21 @@ class WaveformSprite extends MeshRender return super.set_width(value); } - public function new(waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + /** + * The minimum size, in pixels, that a waveform will display with. + * Useful for preventing the waveform from becoming too small to see. + * + * NOTE: This is technically doubled since it's applied above and below the center of the waveform. + */ + public var minWaveformSize:Int = 1; + + /** + * A multiplier on the size of the waveform. + * Still capped at the width and height set for the sprite. + */ + public var amplitude:Float = 1.0; + + public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) { super(DEFAULT_X, DEFAULT_Y, DEFAULT_COLOR); this.waveformColor = color ?? DEFAULT_COLOR; @@ -164,6 +177,8 @@ class WaveformSprite extends MeshRender this.clear(); + if (waveformData == null) return; + // Center point of the waveform. When horizontal this is half the height, when vertical this is half the width. var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2); @@ -200,17 +215,28 @@ class WaveformSprite extends MeshRender { var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); - var isOutsideClipRectHorizontal:Bool = (clipRect != null) && (pixelPos < clipRect.x || pixelPos > (clipRect.x + clipRect.width)); - var isOutsideClipRectVertical:Bool = (clipRect != null) && (pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height)); - var isOutsideClipRect:Bool = orientation == HORIZONTAL ? isOutsideClipRectHorizontal : isOutsideClipRectVertical; + var isOutsideClipRect:Bool = (clipRect != null) && if (orientation == HORIZONTAL) + { + pixelPos < clipRect.x + || pixelPos > (clipRect.x + clipRect.width); + } else + { + pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height); + }; // This index is outside the clipRect, so we can just skip rendering it. Fantastic! if (isOutsideClipRect) continue; - var vertexTopY:Int = Std.int(waveformCenterPos - - (waveformData.channel(0).maxSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); - var vertexBottomY:Int = Std.int(waveformCenterPos - + (-waveformData.channel(0).minSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleMapped(i) * amplitude, 1.0); + var sampleMin:Float = Math.max(waveformData.channel(0).minSampleMapped(i) * amplitude, -1.0); + var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2; + if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize; + var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2; + if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize; + var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize); + var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize); + + if (vertexBottomY - vertexTopY < minWaveformSize) vertexTopY = vertexBottomY - minWaveformSize; var vertexTopIndex:Int = -1; var vertexBottomIndex:Int = -1; @@ -271,22 +297,31 @@ class WaveformSprite extends MeshRender var waveformLengthPixels:Int = orientation == HORIZONTAL ? Std.int(this.width) : Std.int(this.height); for (i in 0...waveformLengthPixels) { - // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. - var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); - var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); var pixelPos:Int = i; - var isOutsideClipRectHorizontal:Bool = (clipRect != null) && (pixelPos < clipRect.x || pixelPos > (clipRect.x + clipRect.width)); - var isOutsideClipRectVertical:Bool = (clipRect != null) && (pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height)); - var isOutsideClipRect:Bool = orientation == HORIZONTAL ? isOutsideClipRectHorizontal : isOutsideClipRectVertical; - + var isOutsideClipRect:Bool = (clipRect != null) && if (orientation == HORIZONTAL) + { + pixelPos < clipRect.x + || pixelPos > (clipRect.x + clipRect.width); + } else + { + pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height); + }; // This index is outside the clipRect, so we can just skip rendering it. Fantastic! if (isOutsideClipRect) continue; - var vertexTopY:Int = Std.int(waveformCenterPos - - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); - var vertexBottomY:Int = Std.int(waveformCenterPos - + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. + var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); + var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); + + var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * amplitude, 1.0); + var sampleMin:Float = Math.max(waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * amplitude, -1.0); + var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2; + if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize; + var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2; + if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize; + var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize); + var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize); var vertexTopIndex:Int = -1; var vertexBottomIndex:Int = -1; diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx index 354adf0bf7..745ac7a6b9 100644 --- a/source/funkin/ui/debug/WaveformTestState.hx +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -33,19 +33,20 @@ class WaveformTestState extends MusicBeatState var testSprite = new FlxSprite(0, 0); testSprite.loadGraphic(Paths.image('funkay')); testSprite.updateHitbox(); - testSprite.clipRect = new FlxRect(0, 0, FlxG.width * 9 / 16, FlxG.height); + testSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height); add(testSprite); waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect')); - // waveformData = WaveformDataParser.parseWaveformData(Paths.json('waveform/dadbattle-erect/dadbattle-erect.waveform')); waveformData = WaveformDataParser.interpretFlxSound(waveformAudio); - waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"), 2.5); - waveformSprite.width = FlxG.width; + waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6")); + waveformSprite.duration = 5.0 * 160; + waveformSprite.width = FlxG.width * 160; waveformSprite.height = FlxG.height; // / 2; - waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width * 9 / 16, FlxG.height); - // waveformSprite.clipRect = new FlxRect(FlxG.width * 1 / 3, FlxG.height * 3 / 8, FlxG.width * 1 / 3, FlxG.height * 2 / 8); + waveformSprite.amplitude = 2.0; + waveformSprite.minWaveformSize = 25; + waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height); add(waveformSprite); // waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0); From c9163986d5f3371e5e9e036cd5889785e320a841 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 25 Jan 2024 20:23:18 -0500 Subject: [PATCH 11/39] Initial attempt at rendering the waveform inside the offsets toolbox. --- assets | 2 +- hmm.json | 4 +- source/funkin/audio/VoicesGroup.hx | 36 ++++++++++ .../funkin/audio/waveform/WaveformSprite.hx | 37 +++++------ .../toolboxes/ChartEditorOffsetsToolbox.hx | 66 +++++++++++++++++++ .../ui/haxeui/components/WaveformPlayer.hx | 17 +++++ 6 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 source/funkin/ui/haxeui/components/WaveformPlayer.hx diff --git a/assets b/assets index 3d92b49768..685c9481c2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 3d92b497682727d34eaa55e564e0bd9faea1c9d7 +Subproject commit 685c9481c26020563333852d9401fe4c6ee10257 diff --git a/hmm.json b/hmm.json index 7e17f105cf..4b2885a878 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "bb904f8b4b205755a310c23ff25219f9dcd62711", + "ref": "5b2d5b8e7e470cf637953e1369c80a1f42016a75", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "1ec470c297afd7758a90dc9399aa1e3a4ea6ca0b", + "ref": "e9f880522e27134b29df4067f82df7d7e5237b70", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 42f31af70f..c47b775b4b 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -2,6 +2,8 @@ package funkin.audio; import funkin.audio.FunkinSound; import flixel.group.FlxGroup.FlxTypedGroup; +import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformDataParser; class VoicesGroup extends SoundGroup { @@ -104,6 +106,40 @@ class VoicesGroup extends SoundGroup return opponentVolume = volume; } + public function buildPlayerVoiceWaveform():Null + { + if (playerVoices.members.length == 0) return null; + + return WaveformDataParser.interpretFlxSound(playerVoices.members[0]); + } + + public function buildOpponentVoiceWaveform():Null + { + if (opponentVoices.members.length == 0) return null; + + return WaveformDataParser.interpretFlxSound(opponentVoices.members[0]); + } + + /** + * The length of the player's vocal track, in milliseconds. + */ + public function getPlayerVoiceLength():Float + { + if (playerVoices.members.length == 0) return 0.0; + + return playerVoices.members[0].length; + } + + /** + * The length of the opponent's vocal track, in milliseconds. + */ + public function getOpponentVoiceLength():Float + { + if (opponentVoices.members.length == 0) return 0.0; + + return opponentVoices.members[0].length; + } + public override function clear():Void { playerVoices.clear(); diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index 99da306140..ceeab7150b 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -215,18 +215,18 @@ class WaveformSprite extends MeshRender { var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); - var isOutsideClipRect:Bool = (clipRect != null) && if (orientation == HORIZONTAL) - { - pixelPos < clipRect.x - || pixelPos > (clipRect.x + clipRect.width); - } else + var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y); + + if (isBeforeClipRect) continue; + + var isAfterClipRect:Bool = (clipRect != null) + && ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height)); + + if (isAfterClipRect) { - pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height); + break; }; - // This index is outside the clipRect, so we can just skip rendering it. Fantastic! - if (isOutsideClipRect) continue; - var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleMapped(i) * amplitude, 1.0); var sampleMin:Float = Math.max(waveformData.channel(0).minSampleMapped(i) * amplitude, -1.0); var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2; @@ -299,16 +299,17 @@ class WaveformSprite extends MeshRender { var pixelPos:Int = i; - var isOutsideClipRect:Bool = (clipRect != null) && if (orientation == HORIZONTAL) - { - pixelPos < clipRect.x - || pixelPos > (clipRect.x + clipRect.width); - } else + var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y); + + if (isBeforeClipRect) continue; + + var isAfterClipRect:Bool = (clipRect != null) + && ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height)); + + if (isAfterClipRect) { - pixelPos < clipRect.y || pixelPos > (clipRect.y + clipRect.height); + break; }; - // This index is outside the clipRect, so we can just skip rendering it. Fantastic! - if (isOutsideClipRect) continue; // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); @@ -369,8 +370,6 @@ class WaveformSprite extends MeshRender prevVertexBottomIndex = vertexBottomIndex; } } - - trace('Rendering waveform of ${duration} seconds with ${this.vertex_count} vertices.'); } function buildClippedVertex(x:Int, y:Int, topLeftVertexIndex:Int, topRightVertexIndex:Int, bottomLeftVertexIndex:Int, bottomRightVertexIndex:Int):Int diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index a68b8d364c..8dae1f7daa 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -15,9 +15,12 @@ import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; import funkin.play.stage.Stage; +import funkin.ui.haxeui.components.WaveformPlayer; +import funkin.audio.waveform.WaveformDataParser; import haxe.ui.containers.Box; import haxe.ui.containers.Frame; import haxe.ui.events.UIEvent; +import funkin.audio.waveform.WaveformData; /** * The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM. @@ -27,6 +30,14 @@ import haxe.ui.events.UIEvent; @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/offsets.xml")) class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { + var waveformPlayer:WaveformPlayer; + var waveformOpponent:WaveformPlayer; + var waveformInstrumental:WaveformPlayer; + var offsetButtonZoomIn:Button; + var offsetButtonZoomOut:Button; + + var waveformScale:Int = 64; + public function new(chartEditorState2:ChartEditorState) { super(chartEditorState2); @@ -48,12 +59,67 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox this.x = 150; this.y = 250; + offsetButtonZoomIn.onClick = (_) -> { + zoomWaveformIn(); + }; + offsetButtonZoomOut.onClick = (_) -> { + zoomWaveformOut(); + }; + + // Build player waveform. + waveformPlayer.waveform.forceUpdate = true; + waveformPlayer.waveform.waveformData = chartEditorState.audioVocalTrackGroup.buildPlayerVoiceWaveform(); + // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it. + waveformPlayer.waveform.duration = 5.0; // chartEditorState.audioVocalTrackGroup.getPlayerVoiceLength() / 1000; + + // Build opponent waveform. + waveformOpponent.waveform.forceUpdate = true; + waveformOpponent.waveform.waveformData = chartEditorState.audioVocalTrackGroup.buildOpponentVoiceWaveform(); + waveformOpponent.waveform.duration = 5.0; // chartEditorState.audioVocalTrackGroup.getOpponentVoiceLength() / 1000; + + // Build instrumental waveform. + waveformInstrumental.waveform.forceUpdate = true; + waveformInstrumental.waveform.waveformData = WaveformDataParser.interpretFlxSound(chartEditorState.audioInstTrack); + waveformInstrumental.waveform.duration = 5.0; // chartEditorState.audioInstTrack.length / 1000; + + refresh(); + } + + public function zoomWaveformIn():Void + { + if (waveformScale > 1) + { + waveformScale = Std.int(waveformScale / 2); + } + else + { + waveformScale = 1; + } + + refresh(); + } + + public function zoomWaveformOut():Void + { + waveformScale = Std.int(waveformScale * 2); + refresh(); } public override function refresh():Void { super.refresh(); + + // Set the width based on the waveformScale value. + + waveformPlayer.waveform.width = waveformPlayer.waveform.waveformData.length / waveformScale; + trace('Player duration: ${waveformPlayer.waveform.duration}, width: ${waveformPlayer.waveform.width}'); + + waveformOpponent.waveform.width = waveformOpponent.waveform.waveformData.length / waveformScale; + trace('Opponent duration: ${waveformOpponent.waveform.duration}, width: ${waveformOpponent.waveform.width}'); + + waveformInstrumental.waveform.width = waveformInstrumental.waveform.waveformData.length / waveformScale; + trace('Instrumental duration: ${waveformInstrumental.waveform.duration}, width: ${waveformInstrumental.waveform.width}'); } public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox diff --git a/source/funkin/ui/haxeui/components/WaveformPlayer.hx b/source/funkin/ui/haxeui/components/WaveformPlayer.hx new file mode 100644 index 0000000000..da85fbfef7 --- /dev/null +++ b/source/funkin/ui/haxeui/components/WaveformPlayer.hx @@ -0,0 +1,17 @@ +package funkin.ui.haxeui.components; + +import funkin.audio.waveform.WaveformSprite; +import funkin.audio.waveform.WaveformData; +import haxe.ui.backend.flixel.components.SpriteWrapper; + +class WaveformPlayer extends SpriteWrapper +{ + public var waveform(default, null):WaveformSprite; + + public function new(?waveformData:WaveformData) + { + super(); + this.waveform = new WaveformSprite(waveformData); + this.sprite = waveform; + } +} From 8573a725bd0d716f43e93491dddffb6a96de4149 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Fri, 26 Jan 2024 18:19:35 -0700 Subject: [PATCH 12/39] acacagh assets update --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 7e19c4cfa7..b2f8b6a780 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 7e19c4cfa7db57178f03ed4a58a9fd4d2b93dea7 +Subproject commit b2f8b6a780316959d0a79adc6dbf61f9e4ca675f From 4f7c2cd511e3cf0087466bc810978cfadc8e8571 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Fri, 26 Jan 2024 18:30:41 -0700 Subject: [PATCH 13/39] Now using hbox to more easily append unit label --- .../toolboxes/ChartEditorEventDataToolbox.hx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx index 9ae4cbb013..7b163ad3d1 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -18,6 +18,7 @@ import haxe.ui.core.Component; import funkin.data.event.SongEventRegistry; import haxe.ui.components.TextField; import haxe.ui.containers.Box; +import haxe.ui.containers.HBox; import haxe.ui.containers.Frame; import haxe.ui.events.UIEvent; import haxe.ui.data.ArrayDataSource; @@ -214,18 +215,21 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox input.text = field.type; } - target.addComponent(input); + // Putting in a box so we can add a unit label easily if there is one. + var inputBox:HBox = new HBox(); + inputBox.addComponent(input); + // Add a unit label if applicable. if (field.units != null && field.units != "") { var units:Label = new Label(); units.text = field.units; units.verticalAlign = "center"; - units.left = 85; - units.top = 4; - input.addComponent(units); + inputBox.addComponent(units); } + target.addComponent(inputBox); + // Update the value of the event data. input.onChange = function(event:UIEvent) { var value = event.target.value; From bfb032dc7fc98c6067fe4a9aff7fc95a4421be0a Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Fri, 26 Jan 2024 18:51:36 -0700 Subject: [PATCH 14/39] Added units to other song events. --- source/funkin/data/event/SongEventSchema.hx | 7 ++++++- source/funkin/play/event/FocusCameraSongEvent.hx | 4 ++-- source/funkin/play/event/SetCameraBopSongEvent.hx | 6 ++++-- source/funkin/play/event/ZoomCameraSongEvent.hx | 6 ++++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/source/funkin/data/event/SongEventSchema.hx b/source/funkin/data/event/SongEventSchema.hx index c910cbf5a0..ac7e44373e 100644 --- a/source/funkin/data/event/SongEventSchema.hx +++ b/source/funkin/data/event/SongEventSchema.hx @@ -78,7 +78,12 @@ abstract SongEventSchema(SongEventSchemaRaw) { if (field.units == null || field.units == '') return value; - return value + ' ${field.units}'; + var unit:String = field.units; + + // These units look better when placed immediately next to the value, rather than after a space. + if (unit == 'x' || unit == '°') return value + '${unit}'; + + return value + ' ${unit}'; } } diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 034d798947..847df4a60b 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -146,7 +146,7 @@ class FocusCameraSongEvent extends SongEvent defaultValue: 0, step: 10.0, type: SongEventFieldType.FLOAT, - units: "px", + units: "px" }, { name: "y", @@ -154,7 +154,7 @@ class FocusCameraSongEvent extends SongEvent defaultValue: 0, step: 10.0, type: SongEventFieldType.FLOAT, - units: "px", + units: "px" } ]); } diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index d0e01346fe..a82577a5ff 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -78,14 +78,16 @@ class SetCameraBopSongEvent extends SongEvent title: 'Intensity', defaultValue: 1.0, step: 0.1, - type: SongEventFieldType.FLOAT + type: SongEventFieldType.FLOAT, + units: 'x' }, { name: 'rate', - title: 'Rate (beats/zoom)', + title: 'Rate', defaultValue: 4, step: 1, type: SongEventFieldType.INTEGER, + units: 'beats/zoom' } ]); } diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index a35a12e1e9..8091304997 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -106,14 +106,16 @@ class ZoomCameraSongEvent extends SongEvent title: 'Zoom Level', defaultValue: 1.0, step: 0.1, - type: SongEventFieldType.FLOAT + type: SongEventFieldType.FLOAT, + units: 'x' }, { name: 'duration', - title: 'Duration (in steps)', + title: 'Duration', defaultValue: 4.0, step: 0.5, type: SongEventFieldType.FLOAT, + units: 'steps' }, { name: 'ease', From 47430763141d1106a9349bc622e900779f8171ce Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Fri, 26 Jan 2024 19:01:05 -0700 Subject: [PATCH 15/39] Added % as a unit to not include a space for when displaying in the event tooltip --- source/funkin/data/event/SongEventSchema.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/data/event/SongEventSchema.hx b/source/funkin/data/event/SongEventSchema.hx index ac7e44373e..ca63f62f86 100644 --- a/source/funkin/data/event/SongEventSchema.hx +++ b/source/funkin/data/event/SongEventSchema.hx @@ -81,7 +81,7 @@ abstract SongEventSchema(SongEventSchemaRaw) var unit:String = field.units; // These units look better when placed immediately next to the value, rather than after a space. - if (unit == 'x' || unit == '°') return value + '${unit}'; + if (unit == 'x' || unit == '°' || unit == '%') return value + '${unit}'; return value + ' ${unit}'; } From 7137c13382ad729ed286759ba4762ba10f7a7470 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Fri, 26 Jan 2024 19:36:10 -0700 Subject: [PATCH 16/39] Copy command now pulses events in selection even if no notes are in the selection. "Copied" prompt now includes events in the text. --- source/funkin/data/song/SongDataUtils.hx | 8 +-- .../charting/commands/CopyItemsCommand.hx | 62 +++++++++++++------ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 275106f3ac..01ea2da32f 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -153,8 +153,8 @@ class SongDataUtils public static function buildNoteClipboard(notes:Array, ?timeOffset:Int = null):Array { if (notes.length == 0) return notes; - if (timeOffset == null) timeOffset = -Std.int(notes[0].time); - return offsetSongNoteData(sortNotes(notes), timeOffset); + if (timeOffset == null) timeOffset = Std.int(notes[0].time); + return offsetSongNoteData(sortNotes(notes), -timeOffset); } /** @@ -165,8 +165,8 @@ class SongDataUtils public static function buildEventClipboard(events:Array, ?timeOffset:Int = null):Array { if (events.length == 0) return events; - if (timeOffset == null) timeOffset = -Std.int(events[0].time); - return offsetSongEventData(sortEvents(events), timeOffset); + if (timeOffset == null) timeOffset = Std.int(events[0].time); + return offsetSongEventData(sortEvents(events), -timeOffset); } /** diff --git a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx index 4361f867fd..6c5152a29c 100644 --- a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx @@ -46,26 +46,14 @@ class CopyItemsCommand implements ChartEditorCommand function performVisuals(state:ChartEditorState):Void { + var hasNotes:Bool = false; + var hasEvents:Bool = false; + + // Wiggle copied notes. if (state.currentNoteSelection.length > 0) { - // Display the "Copied Notes" text. - if (state.txtCopyNotif != null) - { - state.txtCopyNotif.visible = true; - state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard"; - state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2); - state.txtCopyNotif.y = FlxG.mouse.y - 16; - FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5, - { - type: FlxTween.ONESHOT, - ease: FlxEase.quadOut, - onComplete: function(_) { - state.txtCopyNotif.visible = false; - } - }); - } + hasNotes = true; - // Wiggle the notes. for (note in state.renderedNotes.members) { if (state.isNoteSelected(note.noteData)) @@ -91,8 +79,13 @@ class CopyItemsCommand implements ChartEditorCommand }); } } + } + + // Wiggle copied events. + if (state.currentEventSelection.length > 0) + { + hasEvents = true; - // Wiggle the events. for (event in state.renderedEvents.members) { if (state.isEventSelected(event.eventData)) @@ -119,6 +112,39 @@ class CopyItemsCommand implements ChartEditorCommand } } } + + // Display the "Copied Notes" text. + if ((hasNotes || hasEvents) && state.txtCopyNotif != null) + { + var copiedString:String = ''; + if (hasNotes) + { + var copiedNotes:Int = state.currentNoteSelection.length; + copiedString += '${copiedNotes} note'; + if (copiedNotes > 1) copiedString += 's'; + + if (hasEvents) copiedString += ' and '; + } + if (hasEvents) + { + var copiedEvents:Int = state.currentEventSelection.length; + copiedString += '${state.currentEventSelection.length} event'; + if (copiedEvents > 1) copiedString += 's'; + } + + state.txtCopyNotif.visible = true; + state.txtCopyNotif.text = 'Copied ${copiedString} to clipboard'; + state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2); + state.txtCopyNotif.y = FlxG.mouse.y - 16; + FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5, + { + type: FlxTween.ONESHOT, + ease: FlxEase.quadOut, + onComplete: function(_) { + state.txtCopyNotif.visible = false; + } + }); + } } public function undo(state:ChartEditorState):Void From f6e4bc863d135d40acfb3234dc6592ef35f639cb Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 27 Jan 2024 03:24:49 -0500 Subject: [PATCH 17/39] Work in progress on offsets toolbox (working dragging!) --- assets | 2 +- source/funkin/audio/FunkinSound.hx | 25 +- source/funkin/audio/SoundGroup.hx | 9 + source/funkin/audio/VoicesGroup.hx | 10 + .../funkin/audio/waveform/WaveformSprite.hx | 8 + .../ui/debug/charting/ChartEditorState.hx | 21 +- .../handlers/ChartEditorAudioHandler.hx | 8 +- .../toolboxes/ChartEditorMetadataToolbox.hx | 23 -- .../toolboxes/ChartEditorOffsetsToolbox.hx | 331 ++++++++++++++++-- source/funkin/util/logging/CrashHandler.hx | 18 + 10 files changed, 395 insertions(+), 60 deletions(-) diff --git a/assets b/assets index ae904d08df..384a99d732 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ae904d08df53712a582d5a3c26de40ef552422d1 +Subproject commit 384a99d732456b2b3d35fa9bd2e10aa5f747afa6 diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index acae9c6389..2424aca13b 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -7,6 +7,7 @@ import flash.utils.ByteArray; import flixel.sound.FlxSound; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.system.FlxAssets.FlxSoundAsset; +import funkin.util.tools.ICloneable; import openfl.Assets; #if (openfl >= "8.0.0") import openfl.utils.AssetType; @@ -17,10 +18,17 @@ import openfl.utils.AssetType; * - Delayed playback via negative song position. */ @:nullSafety -class FunkinSound extends FlxSound +class FunkinSound extends FlxSound implements ICloneable { static var cache(default, null):FlxTypedGroup = new FlxTypedGroup(); + public var paused(get, never):Bool; + + function get_paused():Bool + { + return this._paused; + } + public var isPlaying(get, never):Bool; function get_isPlaying():Bool @@ -153,6 +161,21 @@ class FunkinSound extends FlxSound return this; } + public function clone():FunkinSound + { + var sound:FunkinSound = new FunkinSound(); + + // Clone the sound by creating one with the same data buffer. + // Reusing the `Sound` object directly causes issues with playback. + @:privateAccess + sound._sound = openfl.media.Sound.fromAudioBuffer(this._sound.__buffer); + + // Call init to ensure the FlxSound is properly initialized. + sound.init(this.looped, this.autoDestroy, this.onComplete); + + return sound; + } + /** * Creates a new `FunkinSound` object. * diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 528aaa80c4..64104fee7b 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -16,6 +16,8 @@ class SoundGroup extends FlxTypedGroup public var pitch(get, set):Float; + public var playing(get, never):Bool; + public function new() { super(); @@ -165,6 +167,13 @@ class SoundGroup extends FlxTypedGroup return time; } + function get_playing():Bool + { + if (getFirstAlive != null) return getFirstAlive().playing; + else + return false; + } + function get_volume():Float { if (getFirstAlive() != null) return getFirstAlive().volume; diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index c47b775b4b..70a01f9dc0 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -106,6 +106,16 @@ class VoicesGroup extends SoundGroup return opponentVolume = volume; } + public function getPlayerVoice(index:Int = 0):Null + { + return playerVoices.members[index]; + } + + public function getOpponentVoice(index:Int = 0):Null + { + return opponentVoices.members[index]; + } + public function buildPlayerVoiceWaveform():Null { if (playerVoices.members.length == 0) return null; diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index ceeab7150b..32ced2fbd3 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -145,6 +145,14 @@ class WaveformSprite extends MeshRender this.forceUpdate = false; } + /** + * Manually tell the waveform to rebuild itself, even if none of its properties have changed. + */ + public function markDirty():Void + { + isWaveformDirty = true; + } + public override function update(elapsed:Float) { super.update(elapsed); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a7d6976cdb..ec6d36b07a 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1438,19 +1438,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return value; } - var currentVocalOffset(get, set):Float; + var currentVocalOffsetPlayer(get, set):Float; - function get_currentVocalOffset():Float + function get_currentVocalOffsetPlayer():Float { - // Currently there's only one vocal offset, so we just grab the player's offset since both should be the same. - // Should probably make it so we can set offsets for player + opponent individually, though. return currentSongOffsets.getVocalOffset(currentPlayerChar); } - function set_currentVocalOffset(value:Float):Float + function set_currentVocalOffsetPlayer(value:Float):Float { - // Currently there's only one vocal offset, so we just apply it to both characters. currentSongOffsets.setVocalOffset(currentPlayerChar, value); + return value; + } + + var currentVocalOffsetOpponent(get, set):Float; + + function get_currentVocalOffsetOpponent():Float + { + return currentSongOffsets.getVocalOffset(currentOpponentChar); + } + + function set_currentVocalOffsetOpponent(value:Float):Float + { currentSongOffsets.setVocalOffset(currentOpponentChar, value); return value; } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 8f60a07803..7f0a1e1557 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -204,7 +204,7 @@ class ChartEditorAudioHandler trace('[WARN] Failed to parse waveform data for vocal track.'); } - state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffset; + state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer; return true; case DAD: state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); @@ -227,7 +227,7 @@ class ChartEditorAudioHandler trace('[WARN] Failed to parse waveform data for vocal track.'); } - state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffset; + state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent; return true; case OTHER: @@ -248,9 +248,9 @@ class ChartEditorAudioHandler { state.audioVocalTrackGroup.clear(); } - if (state.audioVisGroup != null) + if (state.audioWaveforms != null) { - state.audioVisGroup.clearAllVis(); + state.audioWaveforms.clear(); } } diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx index 46c2b19aac..5d8c25bae9 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx @@ -35,8 +35,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox var buttonCharacterGirlfriend:Button; var buttonCharacterOpponent:Button; var inputBPM:NumberStepper; - var inputOffsetInst:NumberStepper; - var inputOffsetVocal:NumberStepper; var labelScrollSpeed:Label; var inputScrollSpeed:Slider; var frameVariation:Frame; @@ -138,25 +136,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox chartEditorState.updateTimeSignature(); }; - inputOffsetInst.onChange = function(event:UIEvent) { - if (event.value == null) return; - - chartEditorState.currentInstrumentalOffset = event.value; - Conductor.instance.instrumentalOffset = event.value; - // Update song length. - chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset; - }; - - inputOffsetVocal.onChange = function(event:UIEvent) { - if (event.value == null) return; - - chartEditorState.currentVocalOffset = event.value; - if (chartEditorState.audioVocalTrackGroup != null) - { - chartEditorState.audioVocalTrackGroup.playerVoicesOffset = event.value; - chartEditorState.audioVocalTrackGroup.opponentVoicesOffset = event.value; - } - }; inputScrollSpeed.onChange = function(event:UIEvent) { var valid:Bool = event.target.value != null && event.target.value > 0; @@ -196,8 +175,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox inputStage.value = chartEditorState.currentSongMetadata.playData.stage; inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle; inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm; - inputOffsetInst.value = chartEditorState.currentSongMetadata.offsets.getInstrumentalOffset(); - inputOffsetVocal.value = chartEditorState.currentSongMetadata.offsets.getVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player); inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed; labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x'; frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}'; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index 8dae1f7daa..56b602758a 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -1,42 +1,54 @@ package funkin.ui.debug.charting.toolboxes; -import funkin.play.character.BaseCharacter.CharacterType; -import funkin.play.character.CharacterData; -import funkin.data.stage.StageData; -import funkin.data.stage.StageRegistry; -import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; -import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import funkin.audio.SoundGroup; import haxe.ui.components.Button; -import haxe.ui.components.CheckBox; -import haxe.ui.components.DropDown; import haxe.ui.components.HorizontalSlider; import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; -import haxe.ui.components.TextField; -import funkin.play.stage.Stage; import funkin.ui.haxeui.components.WaveformPlayer; import funkin.audio.waveform.WaveformDataParser; -import haxe.ui.containers.Box; +import haxe.ui.containers.VBox; +import haxe.ui.containers.ScrollView; import haxe.ui.containers.Frame; +import haxe.ui.core.Screen; +import haxe.ui.events.DragEvent; +import haxe.ui.events.MouseEvent; import haxe.ui.events.UIEvent; -import funkin.audio.waveform.WaveformData; /** * The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM. */ // @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. + @:access(funkin.ui.debug.charting.ChartEditorState) @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/offsets.xml")) class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { + var waveformContainer:VBox; + var waveformScrollview:ScrollView; var waveformPlayer:WaveformPlayer; var waveformOpponent:WaveformPlayer; var waveformInstrumental:WaveformPlayer; var offsetButtonZoomIn:Button; var offsetButtonZoomOut:Button; + var offsetButtonPause:Button; + var offsetButtonPlay:Button; + var offsetButtonStop:Button; + var offsetStepperPlayer:NumberStepper; + var offsetStepperOpponent:NumberStepper; + var offsetStepperInstrumental:NumberStepper; + + static final BASE_SCALE:Int = 64; + + var waveformScale:Int = BASE_SCALE; + + var audioPreviewTracks:SoundGroup; - var waveformScale:Int = 64; + // Local store of the audio offsets, so we can detect when they change. + var audioPreviewPlayerOffset:Float = 0; + var audioPreviewOpponentOffset:Float = 0; + var audioPreviewInstrumentalOffset:Float = 0; public function new(chartEditorState2:ChartEditorState) { @@ -65,23 +77,222 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox offsetButtonZoomOut.onClick = (_) -> { zoomWaveformOut(); }; + offsetButtonPause.onClick = (_) -> { + pauseAudioPreview(); + }; + offsetButtonPlay.onClick = (_) -> { + playAudioPreview(); + }; + offsetButtonStop.onClick = (_) -> { + stopAudioPreview(); + }; + offsetStepperPlayer.onChange = (event:UIEvent) -> { + chartEditorState.currentVocalOffsetPlayer = event.value; + refresh(); + } + offsetStepperOpponent.onChange = (event:UIEvent) -> { + chartEditorState.currentVocalOffsetOpponent = event.value; + refresh(); + } + offsetStepperInstrumental.onChange = (event:UIEvent) -> { + chartEditorState.currentInstrumentalOffset = event.value; + refresh(); + } + waveformScrollview.onScroll = (_) -> { + if (!audioPreviewTracks.playing) + { + // We have to change the song position to match. + var currentWaveformIndex:Int = Std.int(waveformScrollview.hscrollPos / BASE_SCALE * waveformScale); + var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex); + audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC; + addOffsetsToAudioPreview(); + } + else + { + // The scrollview probably changed because the song position changed. + // If we try to move the song now it will glitch. + } + + // Either way, clipRect has changed, so we need to refresh the waveforms. + refresh(); + }; // Build player waveform. - waveformPlayer.waveform.forceUpdate = true; + // waveformPlayer.waveform.forceUpdate = true; waveformPlayer.waveform.waveformData = chartEditorState.audioVocalTrackGroup.buildPlayerVoiceWaveform(); // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it. - waveformPlayer.waveform.duration = 5.0; // chartEditorState.audioVocalTrackGroup.getPlayerVoiceLength() / 1000; + waveformPlayer.waveform.duration = chartEditorState.audioVocalTrackGroup.getPlayerVoiceLength() / Constants.MS_PER_SEC; // Build opponent waveform. - waveformOpponent.waveform.forceUpdate = true; + // waveformOpponent.waveform.forceUpdate = true; waveformOpponent.waveform.waveformData = chartEditorState.audioVocalTrackGroup.buildOpponentVoiceWaveform(); - waveformOpponent.waveform.duration = 5.0; // chartEditorState.audioVocalTrackGroup.getOpponentVoiceLength() / 1000; + waveformOpponent.waveform.duration = chartEditorState.audioVocalTrackGroup.getOpponentVoiceLength() / Constants.MS_PER_SEC; // Build instrumental waveform. - waveformInstrumental.waveform.forceUpdate = true; + // waveformInstrumental.waveform.forceUpdate = true; waveformInstrumental.waveform.waveformData = WaveformDataParser.interpretFlxSound(chartEditorState.audioInstTrack); - waveformInstrumental.waveform.duration = 5.0; // chartEditorState.audioInstTrack.length / 1000; + waveformInstrumental.waveform.duration = chartEditorState.audioInstTrack.length / Constants.MS_PER_SEC; + + refresh(); + refreshAudioPreview(); + + waveformPlayer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(PLAYER); + }); + waveformOpponent.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(OPPONENT); + }); + waveformInstrumental.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> { + onStartDragWaveform(INSTRUMENTAL); + }); + } + + /** + * Pull the audio tracks from the chart editor state and create copies of them to play in the Offsets Toolbox. + * These must be DEEP CLONES or else the editor will affect the audio preview! + */ + public function refreshAudioPreview():Void + { + if (audioPreviewTracks == null) + { + audioPreviewTracks = new SoundGroup(); + // Make sure audioPreviewTracks (and all its children) receives update() calls. + chartEditorState.add(audioPreviewTracks); + } + else + { + audioPreviewTracks.stop(); + audioPreviewTracks.clear(); + } + + audioPreviewTracks.add(chartEditorState.audioInstTrack.clone()); + audioPreviewTracks.add(chartEditorState.audioVocalTrackGroup.getPlayerVoice().clone()); + audioPreviewTracks.add(chartEditorState.audioVocalTrackGroup.getOpponentVoice().clone()); + + addOffsetsToAudioPreview(); + } + + var dragMousePosition:Float = 0; + var dragWaveform:Waveform = null; + + public function onStartDragWaveform(waveform:Waveform):Void + { + dragMousePosition = FlxG.mouse.x; + dragWaveform = waveform; + + Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragWaveform); + Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragWaveform); + } + + public function onDragWaveform(event:MouseEvent):Void + { + var newDragMousePosition = FlxG.mouse.x; + var deltaMousePosition = newDragMousePosition - dragMousePosition; + + if (deltaMousePosition == 0) return; + + var deltaPixels:Float = deltaMousePosition / BASE_SCALE * waveformScale; + var deltaMilliseconds:Float = switch (dragWaveform) + { + case PLAYER: + deltaPixels / waveformPlayer.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + case OPPONENT: + deltaPixels / waveformOpponent.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + case INSTRUMENTAL: + deltaPixels / waveformInstrumental.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC; + }; + + trace('Moving waveform by ${deltaMousePosition} -> ${deltaPixels} -> ${deltaMilliseconds} milliseconds.'); + + switch (dragWaveform) + { + case PLAYER: + chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds; + case OPPONENT: + chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds; + case INSTRUMENTAL: + chartEditorState.currentInstrumentalOffset += deltaMilliseconds; + } + + dragMousePosition = newDragMousePosition; + + refresh(); + } + + public function onStopDragWaveform(event:MouseEvent):Void + { + // Stop dragging. + Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragWaveform); + Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragWaveform); + + dragMousePosition = 0; + dragWaveform = null; + } + + public function playAudioPreview():Void + { + // chartEditorState.stopAudioPlayback(); + audioPreviewTracks.resume(); + } + + public function addOffsetsToAudioPreview():Void + { + var trackInst = audioPreviewTracks.members[0]; + if (trackInst != null) + { + audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset; + trackInst.time -= audioPreviewInstrumentalOffset; + } + + var trackPlayer = audioPreviewTracks.members[1]; + if (trackPlayer != null) + { + audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer; + trackPlayer.time -= audioPreviewPlayerOffset; + } + + var trackOpponent = audioPreviewTracks.members[2]; + if (trackOpponent != null) + { + audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; + trackOpponent.time -= audioPreviewOpponentOffset; + } + } + + public function pauseAudioPreview():Void + { + audioPreviewTracks.pause(); + } + + public function stopAudioPreview():Void + { + audioPreviewTracks.stop(); + + audioPreviewTracks.time = 0; + + var trackInst = audioPreviewTracks.members[0]; + if (trackInst != null) + { + audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset; + trackInst.time = -audioPreviewInstrumentalOffset; + } + + var trackPlayer = audioPreviewTracks.members[1]; + if (trackPlayer != null) + { + audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer; + trackPlayer.time = -audioPreviewPlayerOffset; + } + + var trackOpponent = audioPreviewTracks.members[2]; + if (trackOpponent != null) + { + audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; + trackOpponent.time = -audioPreviewOpponentOffset; + } + + waveformScrollview.hscrollPos = 0; refresh(); } @@ -96,6 +307,8 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox waveformScale = 1; } + trace('Zooming in, scale: ${waveformScale}'); + refresh(); } @@ -103,23 +316,84 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { waveformScale = Std.int(waveformScale * 2); + trace('Zooming out, scale: ${waveformScale}'); + refresh(); } + public override function update(elapsed:Float) + { + super.update(elapsed); + + if (audioPreviewTracks.playing) + { + trace('Playback time: ${audioPreviewTracks.time}'); + + var targetScrollPos:Float = waveformPlayer.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / waveformScale * BASE_SCALE; + waveformScrollview.hscrollPos = targetScrollPos; + } + + if (chartEditorState.currentInstrumentalOffset != audioPreviewInstrumentalOffset) + { + var track = audioPreviewTracks.members[0]; + if (track != null) + { + track.time += audioPreviewInstrumentalOffset; + track.time -= chartEditorState.currentInstrumentalOffset; + audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset; + } + } + if (chartEditorState.currentVocalOffsetPlayer != audioPreviewPlayerOffset) + { + var track = audioPreviewTracks.members[1]; + if (track != null) + { + track.time += audioPreviewPlayerOffset; + track.time -= chartEditorState.currentVocalOffsetPlayer; + audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer; + } + } + if (chartEditorState.currentVocalOffsetOpponent != audioPreviewOpponentOffset) + { + var track = audioPreviewTracks.members[2]; + if (track != null) + { + track.time += audioPreviewOpponentOffset; + track.time -= chartEditorState.currentVocalOffsetOpponent; + audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent; + } + } + } + public override function refresh():Void { super.refresh(); // Set the width based on the waveformScale value. - waveformPlayer.waveform.width = waveformPlayer.waveform.waveformData.length / waveformScale; - trace('Player duration: ${waveformPlayer.waveform.duration}, width: ${waveformPlayer.waveform.width}'); + var maxWidth:Int = -1; + + offsetStepperPlayer.value = chartEditorState.currentVocalOffsetPlayer; + offsetStepperOpponent.value = chartEditorState.currentVocalOffsetOpponent; + offsetStepperInstrumental.value = chartEditorState.currentInstrumentalOffset; - waveformOpponent.waveform.width = waveformOpponent.waveform.waveformData.length / waveformScale; - trace('Opponent duration: ${waveformOpponent.waveform.duration}, width: ${waveformOpponent.waveform.width}'); + waveformPlayer.waveform.time = -chartEditorState.currentVocalOffsetPlayer / Constants.MS_PER_SEC; // Negative offsets make the song start early. + waveformPlayer.waveform.width = waveformPlayer.waveform.waveformData.length / waveformScale * BASE_SCALE; + if (waveformPlayer.waveform.width > maxWidth) maxWidth = Std.int(waveformPlayer.waveform.width); - waveformInstrumental.waveform.width = waveformInstrumental.waveform.waveformData.length / waveformScale; - trace('Instrumental duration: ${waveformInstrumental.waveform.duration}, width: ${waveformInstrumental.waveform.width}'); + waveformOpponent.waveform.time = -chartEditorState.currentVocalOffsetOpponent / Constants.MS_PER_SEC; + waveformOpponent.waveform.width = waveformOpponent.waveform.waveformData.length / waveformScale * BASE_SCALE; + if (waveformOpponent.waveform.width > maxWidth) maxWidth = Std.int(waveformOpponent.waveform.width); + + waveformInstrumental.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC; + waveformInstrumental.waveform.width = waveformInstrumental.waveform.waveformData.length / waveformScale * BASE_SCALE; + if (waveformInstrumental.waveform.width > maxWidth) maxWidth = Std.int(waveformInstrumental.waveform.width); + + waveformPlayer.waveform.markDirty(); + waveformOpponent.waveform.markDirty(); + waveformInstrumental.waveform.markDirty(); + + waveformContainer.width = maxWidth; } public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox @@ -127,3 +401,10 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox return new ChartEditorOffsetsToolbox(chartEditorState); } } + +enum Waveform +{ + PLAYER; + OPPONENT; + INSTRUMENTAL; +} diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index 82db68d30b..ad5983e523 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -199,6 +199,24 @@ class CrashHandler throw "This is an example of an uncaught exception."; } + public static function induceNullObjectReference():Void + { + var obj:Dynamic = null; + var value = obj.test; + } + + public static function induceNullObjectReference2():Void + { + var obj:Dynamic = null; + var value = obj.test(); + } + + public static function induceNullObjectReference3():Void + { + var obj:Dynamic = null; + var value = obj(); + } + static function renderMethod():String { return switch (FlxG.renderMethod) From c23ddb5c5a5d6d738b8fe808a2a203ee539e672a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 30 Jan 2024 21:49:34 -0500 Subject: [PATCH 18/39] Fixes to the MeshRenderer --- source/funkin/graphics/rendering/MeshRender.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/funkin/graphics/rendering/MeshRender.hx b/source/funkin/graphics/rendering/MeshRender.hx index 6a781beae9..a06d533377 100644 --- a/source/funkin/graphics/rendering/MeshRender.hx +++ b/source/funkin/graphics/rendering/MeshRender.hx @@ -97,6 +97,8 @@ class MeshRender extends FlxStrip { vertices.length = 0; indices.length = 0; + uvtData.length = 0; + colors.length = 0; vertex_count = 0; index_count = 0; } From 0f5be08c416349ee2925a389b9dfa2adb0145fe2 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 30 Jan 2024 21:49:49 -0500 Subject: [PATCH 19/39] Add mute and max volume options to FunkinSound. --- source/funkin/audio/FunkinSound.hx | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 2424aca13b..bc35cc0a7d 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -8,6 +8,7 @@ import flixel.sound.FlxSound; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.system.FlxAssets.FlxSoundAsset; import funkin.util.tools.ICloneable; +import flixel.math.FlxMath; import openfl.Assets; #if (openfl >= "8.0.0") import openfl.utils.AssetType; @@ -20,8 +21,29 @@ import openfl.utils.AssetType; @:nullSafety class FunkinSound extends FlxSound implements ICloneable { + static final MAX_VOLUME:Float = 2.0; + static var cache(default, null):FlxTypedGroup = new FlxTypedGroup(); + public var muted(default, set):Bool = false; + + function set_muted(value:Bool):Bool + { + if (value == muted) return value; + muted = value; + updateTransform(); + return value; + } + + override function set_volume(value:Float):Float + { + // Uncap the volume. + fixMaxVolume(); + _volume = FlxMath.bound(value, 0.0, MAX_VOLUME); + updateTransform(); + return _volume; + } + public var paused(get, never):Bool; function get_paused():Bool @@ -84,6 +106,17 @@ class FunkinSound extends FlxSound implements ICloneable return this; } + function fixMaxVolume():Void + { + #if lime_openal + // This code is pretty fragile, it reaches through 5 layers of private access. + @:privateAccess + var handle = this?._channel?.__source?.__backend?.handle; + if (handle == null) return; + lime.media.openal.AL.sourcef(handle, lime.media.openal.AL.MAX_GAIN, MAX_VOLUME); + #end + } + public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound { if (!exists) return this; @@ -161,6 +194,18 @@ class FunkinSound extends FlxSound implements ICloneable return this; } + /** + * Call after adjusting the volume to update the sound channel's settings. + */ + @:allow(flixel.sound.FlxSoundGroup) + override function updateTransform():Void + { + _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end + (group != null ? group.volume : 1) * _volume * _volumeAdjust; + + if (_channel != null) _channel.soundTransform = _transform; + } + public function clone():FunkinSound { var sound:FunkinSound = new FunkinSound(); From f74604c48ef20e19f33ab7098eef5b8bcff73001 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 30 Jan 2024 21:50:25 -0500 Subject: [PATCH 20/39] Feature changes to Offsets window --- assets | 2 +- source/funkin/ui/debug/WaveformTestState.hx | 114 ++++++++--- .../toolboxes/ChartEditorOffsetsToolbox.hx | 177 +++++++++++++++++- 3 files changed, 262 insertions(+), 31 deletions(-) diff --git a/assets b/assets index 384a99d732..9350711c41 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 384a99d732456b2b3d35fa9bd2e10aa5f747afa6 +Subproject commit 9350711c41ce30151ff6a46cadaf0703461486d3 diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx index 745ac7a6b9..f3566db853 100644 --- a/source/funkin/ui/debug/WaveformTestState.hx +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -21,11 +21,13 @@ class WaveformTestState extends MusicBeatState var waveformAudio:FunkinSound; - var waveformSprite:WaveformSprite; - + // var waveformSprite:WaveformSprite; // var waveformSprite2:WaveformSprite; var timeMarker:FlxSprite; + var polygonSprite:MeshRender; + var vertexCount:Int = 3; + public override function create():Void { super.create(); @@ -34,21 +36,26 @@ class WaveformTestState extends MusicBeatState testSprite.loadGraphic(Paths.image('funkay')); testSprite.updateHitbox(); testSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height); - add(testSprite); + // add(testSprite); waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect')); waveformData = WaveformDataParser.interpretFlxSound(waveformAudio); - waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6")); - waveformSprite.duration = 5.0 * 160; - waveformSprite.width = FlxG.width * 160; - waveformSprite.height = FlxG.height; // / 2; - waveformSprite.amplitude = 2.0; - waveformSprite.minWaveformSize = 25; - waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height); - add(waveformSprite); + polygonSprite = new MeshRender(FlxG.width / 2, FlxG.height / 2, FlxColor.WHITE); + + setPolygonVertices(vertexCount); + add(polygonSprite); + // waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6")); + // waveformSprite.duration = 5.0 * 160; + // waveformSprite.width = FlxG.width * 160; + // waveformSprite.height = FlxG.height; // / 2; + // waveformSprite.amplitude = 2.0; + // waveformSprite.minWaveformSize = 25; + // waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height); + // add(waveformSprite); + // // waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0); // waveformSprite2.width = FlxG.width; // waveformSprite2.height = FlxG.height / 2; @@ -80,48 +87,103 @@ class WaveformTestState extends MusicBeatState if (FlxG.keys.justPressed.ENTER) { - if (waveformSprite.orientation == HORIZONTAL) - { - waveformSprite.orientation = VERTICAL; - // waveformSprite2.orientation = VERTICAL; - } - else - { - waveformSprite.orientation = HORIZONTAL; - // waveformSprite2.orientation = HORIZONTAL; - } + // if (waveformSprite.orientation == HORIZONTAL) + // { + // // waveformSprite.orientation = VERTICAL; + // // waveformSprite2.orientation = VERTICAL; + // } + // else + // { + // // waveformSprite.orientation = HORIZONTAL; + // // waveformSprite2.orientation = HORIZONTAL; + // } } if (waveformAudio.isPlaying) { // waveformSprite takes a time in fractional seconds, not milliseconds. var timeSeconds = waveformAudio.time / 1000; - waveformSprite.time = timeSeconds; + // waveformSprite.time = timeSeconds; // waveformSprite2.time = timeSeconds; } if (FlxG.keys.justPressed.UP) { - waveformSprite.duration += 1.0; + vertexCount += 1; + setPolygonVertices(vertexCount); + // waveformSprite.duration += 1.0; // waveformSprite2.duration += 1.0; } if (FlxG.keys.justPressed.DOWN) { - waveformSprite.duration -= 1.0; + vertexCount -= 1; + setPolygonVertices(vertexCount); + // waveformSprite.duration -= 1.0; // waveformSprite2.duration -= 1.0; } if (FlxG.keys.justPressed.LEFT) { - waveformSprite.time -= 1.0; + // waveformSprite.time -= 1.0; // waveformSprite2.time -= 1.0; } if (FlxG.keys.justPressed.RIGHT) { - waveformSprite.time += 1.0; + // waveformSprite.time += 1.0; // waveformSprite2.time += 1.0; } } + function setPolygonVertices(count:Int) + { + polygonSprite.clear(); + + var size = 100.0; + + // Build a polygon with count vertices. + + var vertices:Array> = []; + + var angle = 0.0; + + for (i in 0...count) + { + var x = Math.cos(angle) * size; + var y = Math.sin(angle) * size; + + vertices.push([x, y]); + + angle += 2 * Math.PI / count; + } + + trace('vertices: ${vertices}'); + + var centerVertex = polygonSprite.build_vertex(0, 0); + var firstVertex = -1; + var lastVertex = -1; + + for (vertex in vertices) + { + var x = vertex[0]; + var y = vertex[1]; + + var newVertex = polygonSprite.build_vertex(x, y); + + if (firstVertex == -1) + { + firstVertex = newVertex; + } + + if (lastVertex != -1) + { + polygonSprite.add_tri(centerVertex, lastVertex, newVertex); + } + + lastVertex = newVertex; + } + + polygonSprite.add_tri(centerVertex, lastVertex, firstVertex); + } + public override function destroy():Void { super.destroy(); diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index 56b602758a..c72c7fd9a1 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -39,9 +39,11 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox var offsetStepperOpponent:NumberStepper; var offsetStepperInstrumental:NumberStepper; - static final BASE_SCALE:Int = 64; + static final BASE_SCALE:Float = 64.0; + static final MIN_SCALE:Float = 4.0; + static final WAVEFORM_ZOOM_MULT:Float = 1.5; - var waveformScale:Int = BASE_SCALE; + var waveformScale:Float = BASE_SCALE; var audioPreviewTracks:SoundGroup; @@ -71,6 +73,36 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox 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(); }; @@ -300,7 +332,8 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { if (waveformScale > 1) { - waveformScale = Std.int(waveformScale / 2); + waveformScale = waveformScale / WAVEFORM_ZOOM_MULT; + if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE; } else { @@ -314,13 +347,146 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox public function zoomWaveformOut():Void { - waveformScale = Std.int(waveformScale * 2); + waveformScale = waveformScale * WAVEFORM_ZOOM_MULT; + if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE; trace('Zooming out, scale: ${waveformScale}'); refresh(); } + 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); @@ -380,14 +546,17 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox waveformPlayer.waveform.time = -chartEditorState.currentVocalOffsetPlayer / Constants.MS_PER_SEC; // Negative offsets make the song start early. waveformPlayer.waveform.width = waveformPlayer.waveform.waveformData.length / waveformScale * BASE_SCALE; if (waveformPlayer.waveform.width > maxWidth) maxWidth = Std.int(waveformPlayer.waveform.width); + waveformPlayer.waveform.height = 65; waveformOpponent.waveform.time = -chartEditorState.currentVocalOffsetOpponent / Constants.MS_PER_SEC; waveformOpponent.waveform.width = waveformOpponent.waveform.waveformData.length / waveformScale * BASE_SCALE; if (waveformOpponent.waveform.width > maxWidth) maxWidth = Std.int(waveformOpponent.waveform.width); + waveformOpponent.waveform.height = 65; waveformInstrumental.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC; waveformInstrumental.waveform.width = waveformInstrumental.waveform.waveformData.length / waveformScale * BASE_SCALE; if (waveformInstrumental.waveform.width > maxWidth) maxWidth = Std.int(waveformInstrumental.waveform.width); + waveformInstrumental.waveform.height = 65; waveformPlayer.waveform.markDirty(); waveformOpponent.waveform.markDirty(); From fe7ffecc0dbad44a0afbcddcd2e4eb0ebfd9279b Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Thu, 1 Feb 2024 13:52:50 -0700 Subject: [PATCH 21/39] Chart editor: Tweaked FlxCamera management when entering and exiting playtest mode to fix UI-related crashes. --- .../ui/debug/charting/ChartEditorState.hx | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 5f526a364f..467e36f740 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -690,6 +690,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var activeToolboxes:Map = new Map(); + /** + * The camera component we're using for this state. + */ + var uiCamera:FlxCamera; + // Audio /** @@ -2028,7 +2033,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState loadPreferences(); - fixCamera(); + uiCamera = new FlxCamera(); + FlxG.cameras.reset(uiCamera); buildDefaultSongData(); @@ -5287,7 +5293,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState Paths.setCurrentLevel('weekend1'); } - subStateClosed.add(fixCamera); + subStateClosed.add(reviveUICamera); subStateClosed.add(resetConductorAfterTest); FlxTransitionableState.skipNextTransIn = false; @@ -5312,6 +5318,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; + // Kill and replace the UI camera so it doesn't get destroyed during the state transition. + uiCamera.kill(); + FlxG.cameras.remove(uiCamera, false); + FlxG.cameras.reset(new FlxCamera()); + this.persistentUpdate = false; this.persistentDraw = false; stopWelcomeMusic(); @@ -5401,13 +5412,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } /** - * Fix a camera issue caused when closing the PlayState used when testing. + * Revive the UI camera and re-establish it as the main camera so UI elements depending on it don't explode. */ - function fixCamera(_:FlxSubState = null):Void + function reviveUICamera(_:FlxSubState = null):Void { - FlxG.cameras.reset(new FlxCamera()); - FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2)); - FlxG.camera.zoom = 1.0; + uiCamera.revive(); + FlxG.cameras.reset(uiCamera); add(this.root); } From f6f9ad3985b41efa4c1d63ab1892196814a908d1 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 2 Feb 2024 21:53:45 -0500 Subject: [PATCH 22/39] Completed functionality for offset window. --- source/funkin/audio/waveform/WaveformData.hx | 30 +- .../ui/debug/charting/ChartEditorState.hx | 39 +- .../commands/SetAudioOffsetCommand.hx | 109 ++++++ .../handlers/ChartEditorAudioHandler.hx | 7 +- .../ChartEditorImportExportHandler.hx | 7 +- .../handlers/ChartEditorThemeHandler.hx | 26 +- .../toolboxes/ChartEditorOffsetsToolbox.hx | 369 +++++++++++++++--- 7 files changed, 488 insertions(+), 99 deletions(-) create mode 100644 source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx index 11d3ff641b..31f8dfe02e 100644 --- a/source/funkin/audio/waveform/WaveformData.hx +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -113,7 +113,7 @@ class WaveformData */ public function lenSeconds():Float { - return lenSamples() / sampleRate; + return inline lenSamples() / sampleRate; } /** @@ -121,7 +121,7 @@ class WaveformData */ public function secondsToIndex(seconds:Float):Int { - return Std.int(seconds * pointsPerSecond()); + return Std.int(seconds * inline pointsPerSecond()); } /** @@ -129,7 +129,7 @@ class WaveformData */ public function indexToSeconds(index:Int):Float { - return index / pointsPerSecond(); + return index / inline pointsPerSecond(); } /** @@ -216,7 +216,7 @@ class WaveformDataChannel public function minSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2; - return parent.get(offset); + return inline parent.get(offset); } /** @@ -224,7 +224,7 @@ class WaveformDataChannel */ public function minSampleMapped(i:Int) { - return minSample(i) / parent.maxSampleValue(); + return inline minSample(i) / inline parent.maxSampleValue(); } /** @@ -233,10 +233,10 @@ class WaveformDataChannel */ public function minSampleRange(start:Int, end:Int) { - var min = parent.maxSampleValue(); + var min = inline parent.maxSampleValue(); for (i in start...end) { - var sample = minSample(i); + var sample = inline minSample(i); if (sample < min) min = sample; } return min; @@ -247,7 +247,7 @@ class WaveformDataChannel */ public function minSampleRangeMapped(start:Int, end:Int) { - return minSampleRange(start, end) / parent.maxSampleValue(); + return inline minSampleRange(start, end) / inline parent.maxSampleValue(); } /** @@ -256,7 +256,7 @@ class WaveformDataChannel public function maxSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2 + 1; - return parent.get(offset); + return inline parent.get(offset); } /** @@ -264,7 +264,7 @@ class WaveformDataChannel */ public function maxSampleMapped(i:Int) { - return maxSample(i) / parent.maxSampleValue(); + return inline maxSample(i) / inline parent.maxSampleValue(); } /** @@ -273,10 +273,10 @@ class WaveformDataChannel */ public function maxSampleRange(start:Int, end:Int) { - var max = -parent.maxSampleValue(); + var max = -(inline parent.maxSampleValue()); for (i in start...end) { - var sample = maxSample(i); + var sample = inline maxSample(i); if (sample > max) max = sample; } return max; @@ -287,18 +287,18 @@ class WaveformDataChannel */ public function maxSampleRangeMapped(start:Int, end:Int) { - return maxSampleRange(start, end) / parent.maxSampleValue(); + return inline maxSampleRange(start, end) / inline parent.maxSampleValue(); } public function setMinSample(i:Int, value:Int) { var offset = (i * parent.channels + this.channelId) * 2; - parent.set(offset, value); + inline parent.set(offset, value); } public function setMaxSample(i:Int, value:Int) { var offset = (i * parent.channels + this.channelId) * 2 + 1; - parent.set(offset, value); + inline parent.set(offset, value); } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index ec6d36b07a..719f6aceb6 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,5 +1,6 @@ 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; @@ -1098,7 +1099,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState * `null` until vocal track(s) are loaded. * When switching characters, the elements of the VoicesGroup will be swapped to match the new character. */ - var audioVocalTrackGroup:Null = null; + var audioVocalTrackGroup:VoicesGroup = new VoicesGroup(); /** * The audio waveform visualization for the inst/vocals. @@ -2257,8 +2258,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Initialize the song chart data. songChartData = new Map(); - - audioVocalTrackGroup = new VoicesGroup(); } /** @@ -2889,13 +2888,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemVolumeVocalsPlayer.onChange = event -> { var volume:Float = event.value.toFloat() / 100.0; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.playerVolume = volume; + audioVocalTrackGroup.playerVolume = volume; menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%'; }; menubarItemVolumeVocalsOpponent.onChange = event -> { var volume:Float = event.value.toFloat() / 100.0; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.opponentVolume = volume; + audioVocalTrackGroup.opponentVolume = volume; menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%'; }; @@ -2904,7 +2903,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. #if FLX_PITCH if (audioInstTrack != null) audioInstTrack.pitch = pitch; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; + audioVocalTrackGroup.pitch = pitch; #end var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. menubarLabelPlaybackSpeed.text = 'Playback Speed - ${pitchDisplay}x'; @@ -3215,7 +3214,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState Conductor.instance.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset); // Resync vocals. - if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) { audioVocalTrackGroup.time = audioInstTrack.time; } @@ -3233,7 +3232,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState Conductor.instance.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset); // Resync vocals. - if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) { audioVocalTrackGroup.time = audioInstTrack.time; } @@ -5321,7 +5320,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { FlxG.sound.music = audioInstTrack; } - if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; + targetState.vocals = audioVocalTrackGroup; this.persistentUpdate = false; this.persistentDraw = false; @@ -5433,7 +5432,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (audioInstTrack != null) { audioInstTrack.play(false, audioInstTrack.time); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); + audioVocalTrackGroup.play(false, audioInstTrack.time); } playbarPlay.text = '||'; // Pause @@ -5671,7 +5670,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instance.instrumentalOffset; // Update the songPosition in the Conductor. Conductor.instance.update(audioInstTrack.time); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = audioInstTrack.time; + audioVocalTrackGroup.time = audioInstTrack.time; } // We need to update the note sprites because we changed the scroll position. @@ -5892,7 +5891,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function stopAudioPlayback():Void { if (audioInstTrack != null) audioInstTrack.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + audioVocalTrackGroup.pause(); playbarPlay.text = '>'; } @@ -5927,7 +5926,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Keep the track at the end. audioInstTrack.time = audioInstTrack.length; } - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + audioVocalTrackGroup.pause(); }; } else @@ -5941,12 +5940,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState healthIconsDirty = true; } + function hardRefreshOffsetsToolbox():Void + { + var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT); + if (offsetsToolbox != null) + { + offsetsToolbox.refreshAudioPreview(); + offsetsToolbox.refresh(); + } + } + /** * Clear the voices group. */ public function clearVocals():Void { - if (audioVocalTrackGroup != null) audioVocalTrackGroup.clear(); + audioVocalTrackGroup.clear(); } function isNoteSelected(note:Null):Bool @@ -5971,7 +5980,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Stop the music. if (welcomeMusic != null) welcomeMusic.destroy(); if (audioInstTrack != null) audioInstTrack.destroy(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.destroy(); + audioVocalTrackGroup.destroy(); } function applyCanQuickSave():Void diff --git a/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx b/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx new file mode 100644 index 0000000000..aef4022446 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/SetAudioOffsetCommand.hx @@ -0,0 +1,109 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; + +/** + * Command that copies a given set of notes and song events to the clipboard, + * without deleting them from the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class SetAudioOffsetCommand implements ChartEditorCommand +{ + var type:AudioOffsetType; + var oldOffset:Float = 0; + var newOffset:Float; + var refreshOffsetsToolbox:Bool; + + public function new(type:AudioOffsetType, newOffset:Float, refreshOffsetsToolbox:Bool = true) + { + this.type = type; + this.newOffset = newOffset; + this.refreshOffsetsToolbox = refreshOffsetsToolbox; + } + + public function execute(state:ChartEditorState):Void + { + switch (type) + { + case INSTRUMENTAL: + oldOffset = state.currentInstrumentalOffset; + state.currentInstrumentalOffset = newOffset; + + // Update rendering. + Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; + state.songLengthInMs = (state.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset; + case PLAYER: + oldOffset = state.currentVocalOffsetPlayer; + state.currentVocalOffsetPlayer = newOffset; + + // Update rendering. + state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer; + case OPPONENT: + oldOffset = state.currentVocalOffsetOpponent; + state.currentVocalOffsetOpponent = newOffset; + + // Update rendering. + state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent; + } + + // Update the offsets toolbox. + if (refreshOffsetsToolbox) state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT); + } + + public function undo(state:ChartEditorState):Void + { + switch (type) + { + case INSTRUMENTAL: + state.currentInstrumentalOffset = oldOffset; + + // Update rendering. + Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; + state.songLengthInMs = (state.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset; + case PLAYER: + state.currentVocalOffsetPlayer = oldOffset; + + // Update rendering. + state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer; + case OPPONENT: + state.currentVocalOffsetOpponent = oldOffset; + + // Update rendering. + state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent; + } + + // Update the offsets toolbox. + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT); + } + + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (newOffset != oldOffset); + } + + public function toString():String + { + switch (type) + { + case INSTRUMENTAL: + return 'Set Inst. Audio Offset to $newOffset'; + case PLAYER: + return 'Set Player Audio Offset to $newOffset'; + case OPPONENT: + return 'Set Opponent Audio Offset to $newOffset'; + } + } +} + +enum AudioOffsetType +{ + INSTRUMENTAL; + PLAYER; + OPPONENT; +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 7f0a1e1557..8e40cfc427 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -137,6 +137,8 @@ class ChartEditorAudioHandler result = playVocals(state, DAD, opponentId, instId); // if (!result) return false; + state.hardRefreshOffsetsToolbox(); + return true; } @@ -244,10 +246,7 @@ class ChartEditorAudioHandler public static function stopExistingVocals(state:ChartEditorState):Void { - if (state.audioVocalTrackGroup != null) - { - state.audioVocalTrackGroup.clear(); - } + state.audioVocalTrackGroup.clear(); if (state.audioWaveforms != null) { state.audioWaveforms.clear(); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 9c86269e8f..04d89e3f45 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -132,11 +132,8 @@ class ChartEditorImportExportHandler state.audioInstTrack.stop(); state.audioInstTrack = null; } - if (state.audioVocalTrackGroup != null) - { - state.audioVocalTrackGroup.stop(); - state.audioVocalTrackGroup.clear(); - } + state.audioVocalTrackGroup.stop(); + state.audioVocalTrackGroup.clear(); } /** diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index 9479aa8402..b1af0ce4cb 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -306,10 +306,11 @@ class ChartEditorThemeHandler // Draw the major ticks. var leftTickX:Float = 0; var middleTickX:Float = state.offsetTickBitmap.width / 2 - (majorTickWidth / 2); - var rightTickX:Float = state.offsetTickBitmap.width - majorTickWidth; - state.offsetTickBitmap.fillRect(new Rectangle(leftTickX, 0, majorTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(middleTickX, 0, majorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, majorTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + var rightTickX:Float = state.offsetTickBitmap.width - (majorTickWidth / 2); + var majorTickLength:Float = state.offsetTickBitmap.height; + state.offsetTickBitmap.fillRect(new Rectangle(leftTickX, 0, majorTickWidth / 2, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(middleTickX, 0, majorTickWidth, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, majorTickWidth / 2, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); // Draw the minor ticks. var minorTick2X:Float = state.offsetTickBitmap.width * 1 / 10 - (minorTickWidth / 2); @@ -320,14 +321,15 @@ class ChartEditorThemeHandler var minorTick8X:Float = state.offsetTickBitmap.width * 7 / 10 - (minorTickWidth / 2); var minorTick9X:Float = state.offsetTickBitmap.width * 8 / 10 - (minorTickWidth / 2); var minorTick10X:Float = state.offsetTickBitmap.width * 9 / 10 - (minorTickWidth / 2); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick2X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick3X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick4X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick5X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick7X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick8X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick9X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); - state.offsetTickBitmap.fillRect(new Rectangle(minorTick10X, 0, minorTickWidth, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + var minorTickLength:Float = state.offsetTickBitmap.height * 1 / 3; + state.offsetTickBitmap.fillRect(new Rectangle(minorTick2X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick3X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick4X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick5X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick7X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick8X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick9X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.offsetTickBitmap.fillRect(new Rectangle(minorTick10X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT); // Draw the offset ticks. // var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); // 1 grid squares wide. diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx index c72c7fd9a1..5b26419b23 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx @@ -4,11 +4,16 @@ 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; @@ -25,7 +30,7 @@ import haxe.ui.events.UIEvent; @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/offsets.xml")) class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox { - var waveformContainer:VBox; + var waveformContainer:Absolute; var waveformScrollview:ScrollView; var waveformPlayer:WaveformPlayer; var waveformOpponent:WaveformPlayer; @@ -38,15 +43,56 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox 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