diff --git a/source/funkin/backend/assets/MultiFramesCollection.hx b/source/funkin/backend/assets/MultiFramesCollection.hx index 2a03c64c..d4bed77b 100644 --- a/source/funkin/backend/assets/MultiFramesCollection.hx +++ b/source/funkin/backend/assets/MultiFramesCollection.hx @@ -47,7 +47,7 @@ class MultiFramesCollection extends FlxFramesCollection public function addFrames(collection:FlxFramesCollection) { if (collection == null || collection.frames == null) return; - collection.parent.useCount++; + collection.parent.incrementUseCount(); parentedFrames.push(collection); for(f in collection.frames) { @@ -63,7 +63,7 @@ class MultiFramesCollection extends FlxFramesCollection if(parentedFrames != null) { for(collection in parentedFrames) { if(collection != null) - collection.parent.useCount--; + collection.parent.decrementUseCount(); } parentedFrames = null; } diff --git a/source/funkin/backend/shaders/FunkinShader.hx b/source/funkin/backend/shaders/FunkinShader.hx index 281616b0..6b5ea097 100644 --- a/source/funkin/backend/shaders/FunkinShader.hx +++ b/source/funkin/backend/shaders/FunkinShader.hx @@ -1,17 +1,20 @@ package funkin.backend.shaders; +import haxe.Exception; +import haxe.io.Path; import flixel.graphics.FlxGraphic; import flixel.system.FlxAssets.FlxShader; import flixel.util.FlxSignal.FlxTypedSignal; -import haxe.Exception; +import flixel.util.FlxStringUtil; import hscript.IHScriptCustomBehaviour; -import lime.utils.Float32Array; import openfl.display.BitmapData; import openfl.display.ShaderInput; import openfl.display.ShaderParameter; import openfl.display.ShaderParameterType; +import openfl.display.Shader; import openfl.display3D._internal.GLProgram; import openfl.display3D._internal.GLShader; +import openfl.display3D.Program3D; import openfl.utils._internal.Log; using StringTools; @@ -20,14 +23,94 @@ using StringTools; @:access(openfl.display.ShaderInput) @:access(openfl.display.ShaderParameter) class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { + #if REGION /* Backward Compatibility */ private static var __instanceFields = Type.getInstanceFields(FunkinShader); private static var FRAGMENT_SHADER = 0; private static var VERTEX_SHADER = 1; + public var glslVer(get, set):String; + inline function get_glslVer():String return glVersion; + inline function set_glslVer(v:String):String return glVersion = v; + + public var glRawFragmentSource(get, set):String; + inline function get_glRawFragmentSource():String return __glFragmentSourceRaw; + inline function set_glRawFragmentSource(v:String):String return __glFragmentSourceRaw = v; + + public var glRawVertexSource(get, set):String; + inline function get_glRawVertexSource():String return __glVertexSourceRaw; + inline function set_glRawVertexSource(v:String):String return __glVertexSourceRaw = v; + + // Unused... cne-openfl uses a different system + var __cancelNextProcessGLData:Bool = false; + public var onProcessGLData:FlxTypedSignal<(String, String)->Void> = new FlxTypedSignal<(String, String)->Void>(); + #end + + public static function getShaderCode(key:String, isFragment = true):Null { + var path = "shaders/" + key; + key = Path.withoutExtension(key); + + final ext = Path.extension(path); + if (ext == "") path = path + (isFragment ? ".frag" : ".vert"); + else isFragment = ext != "vert"; + + path = Paths.getPath(path); + return Assets.exists(path) ? Assets.getText(path) : null; + } + + private static function processGLSLText(source:String, glVersion:String, isFragment:Bool, ?pragmas:Map):String + return Shader.processGLSLText(_processGLSLText(source, glVersion, isFragment, pragmas), glVersion, isFragment); + + private static function _processGLSLText(source:String, glVersion:String, isFragment:Bool, ?pragmas:Map):String { + var injectedFragColor = StringTools.contains(source, "out vec4"), canInjectFragColorFix = switch (glVersion) { + case "300 es", "330", "400", "410", "420", "430", "440", "450", "460": isFragment; + default: false; + }; + if (pragmas != null) { + final pragmaKeyword = ~/#pragma (\w+)/g; + source = pragmaKeyword.map(source, (_) -> { + var name = pragmaKeyword.matched(1); + if (!pragmas.exists(name)) return '#pragma $name'; + + var pragma = pragmas.get(name); + if (name == "header" && canInjectFragColorFix && !injectedFragColor) { + injectedFragColor = true; + if (!StringTools.contains(pragma, "out vec4")) return processGLSLText(Shader.fragColorHeaderFix + pragma, glVersion, isFragment, pragmas); + } + return processGLSLText(pragma, glVersion, isFragment, pragmas); + }); + } + + inline function tryGetShaderCode(key:String) { + final s = getShaderCode(key, isFragment); + if (s == null) { + Logs.traceColored([ + Logs.logText('[Shader] ', RED), + Logs.logText('Failed to import shader $key', RED), + ]); + return ""; + } + return s; + } + + final includeKeyword = ~/#include ['"](.+)['"]/g; + final importKeyword = ~/#import\s+<(.*)>/g; + source = importKeyword.map(source, (_) -> + return processGLSLText(tryGetShaderCode(importKeyword.matched(1)), glVersion, isFragment, pragmas) ?? ""); + + return source = includeKeyword.map(source, (_) -> + return processGLSLText(tryGetShaderCode(includeKeyword.matched(1)), glVersion, isFragment, pragmas) ?? ""); + } + + private static var __defaultsAvailable:Bool; + private static var __glFragmentSourceDefault:String; + private static var __glVertexSourceDefault:String; + private static var __glFragmentPragmasDefault:Map; + private static var __glVertexPragmasDefault:Map; + private static var __glFragmentExtensionsDefault:Array; + private static var __glVertexExtensionsDefault:Array; + public var onGLUpdate:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); - public var onProcessGLData:FlxTypedSignal<(String, String)->Void> = new FlxTypedSignal<(String, String)->Void>(); - public var glslVer:String = Flags.DEFAULT_GLSL_VERSION; public var fileName:String = "FunkinShader"; public var fragFileName:String = "FunkinShader"; public var vertFileName:String = "FunkinShader"; @@ -36,221 +119,54 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { public var fragmentPrefix:String = ""; public var vertexPrefix:String = ""; - /** - * Creates a new shader from the specified fragment and vertex source. - * Accepts `#pragma header`. - * @param frag Fragment source (pass `null` to use default) - * @param vert Vertex source (pass `null` to use default) - * @param glslVer Version of GLSL to use (defaults to 120) - */ - public override function new(frag:String, vert:String, glslVer:String = null) { - if (glslVer == null) glslVer = Flags.DEFAULT_GLSL_VERSION; - if (frag == null) frag = ShaderTemplates.defaultFragmentSource; - if (vert == null) vert = ShaderTemplates.defaultVertexSource; - this.glFragmentSource = frag; - this.glVertexSource = vert; + private var __immediate:Bool; + + public function new(?fragmentSource:String, ?vertexSource:String, ?version:String, + ?fragmentExtensions:Array, ?vertexExtensions:Array, immediate = false + ) { + if (!__defaultsAvailable) { + __glFragmentSourceDefault = __glFragmentSourceRaw; + __glVertexSourceDefault = __glVertexSourceRaw; + __glFragmentPragmasDefault = __glFragmentPragmas.copy(); + __glVertexPragmasDefault = __glVertexPragmas.copy(); + __glFragmentExtensionsDefault = __glFragmentExtensions ?? []; + __glVertexExtensionsDefault = __glVertexExtensions ?? []; + __defaultsAvailable = true; + } + + __immediate = immediate; + + if (version != null) glVersion = version; + if (vertexExtensions != null) glVertexExtensions = vertexExtensions; + if (fragmentExtensions != null) glFragmentExtensions = fragmentExtensions; + if (vertexSource != null) glVertexSource = vertexSource; + if (fragmentSource != null) glFragmentSource = fragmentSource; - this.glslVer = glslVer; super(); + + if (!__isGenerated) { + __isGenerated = true; + __init(); + } } - static var IMPORT_REGEX = ~/#import\s+<(.*)>/; + public function loadShader(name:String, ?version:String, immediate = false):FunkinShader { + final fragment = getShaderCode(name, true), vertex = getShaderCode(name, false); - private function processImports(value:String, type:Int):String - { - while(IMPORT_REGEX.match(value)) - { - var importPath = IMPORT_REGEX.matched(1); - var importSource = Assets.getText("assets/shaders/" + importPath); - if(importSource == null) { - var fileName = type == FRAGMENT_SHADER ? fragFileName : vertFileName; - Logs.traceColored([ - Logs.logText('[Shader] ', RED), - Logs.logText('Failed to import shader ${importPath} in ${fileName}', RED), - ]); - } else { - value = value.replace(IMPORT_REGEX.matched(0), importSource); - } - } - return value; + glVersion = version; + glVertexSource = vertex ?? __glVertexSourceDefault; + glFragmentSource = fragment ?? __glFragmentSourceDefault; + + if (immediate) __init(); + return this; } - static var ERROR_POS_REGEX = ~/(\d+):(\d+): (.*)/g; - static var ERROR_REGEX = ~/ERROR: (\d+):(\d+): (.*)/g; - static var ERROR_REGEX_2 = ~/(\d+)\((\d+)\) : error ([^:]+): (.*)/g; - @:noCompletion private override function __createGLShader(source:String, type:Int):GLShader - { - var gl = __context.gl; - - var shader = gl.createShader(type); - gl.shaderSource(shader, source); - gl.compileShader(shader); - var shaderInfoLog = gl.getShaderInfoLog(shader); - var hasInfoLog = shaderInfoLog != null && StringTools.trim(shaderInfoLog) != ""; - var compileStatus = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - - if (hasInfoLog || compileStatus == 0) - { - var isVertexShader = type == gl.VERTEX_SHADER; - var messageBuf = new StringBuf(); - messageBuf.add((compileStatus == 0) ? "Error" : "Info"); - if(isVertexShader) { - messageBuf.add(" compiling vertex shader"); - if(vertFileName != null && vertFileName.length > 0) { - messageBuf.add(" (" + vertFileName + ")"); - } - } else { - messageBuf.add(" compiling fragment shader"); - if(fragFileName != null && fragFileName.length > 0) { - messageBuf.add(" (" + fragFileName + ")"); - } - } - messageBuf.add("\n"); - var errorPositions = []; - var regex = null; - var tmp = shaderInfoLog; - if(shaderInfoLog.contains(" : error ")) { - regex = ERROR_REGEX_2; - - while(regex.match(tmp)) { - errorPositions.push(new ShaderErrorPosition(regex.matched(2), regex.matched(1), regex.matched(4))); - tmp = regex.matchedRight(); - } - } else if(shaderInfoLog.contains("ERROR: ")) { - regex = ERROR_REGEX; - - while(regex.match(tmp)) { - errorPositions.push(new ShaderErrorPosition(regex.matched(2), regex.matched(1), regex.matched(3))); - tmp = regex.matchedRight(); - } - } else { - regex = ERROR_POS_REGEX; - - while(regex.match(tmp)) { - errorPositions.push(new ShaderErrorPosition(regex.matched(2), regex.matched(1), regex.matched(3))); - tmp = regex.matchedRight(); - } - } - var splitSource = source.split("\n"); - for(error in errorPositions) { - messageBuf.add("ERROR: Line: " + error.line); - if(error.column > 0) { - messageBuf.add(", Column: " + error.column); - } - messageBuf.add(", " + error.message); - if(error.line < splitSource.length) { - messageBuf.add("\nLine: "); - messageBuf.add(splitSource[error.line-1].trim()); - } - messageBuf.add("\n\n"); - } - var hasErrorPosition = errorPositions.length > 0; - if(hasErrorPosition) { - messageBuf.add("Raw shader info log:\n"); - } - messageBuf.add(shaderInfoLog); - messageBuf.add("\n"); - messageBuf.add(source); - - var message = messageBuf.toString(); - if (compileStatus == 0) Log.error(message); - else if (hasInfoLog) Log.debug(message); + override function __initGL():Void { + if (__immediate) { + __context = FlxG.stage.context3D; + __enable(); } - - return shader; - } - - @:noCompletion private override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram - { - var program:GLProgram = null; - try - { - var gl = __context.gl; - - var vertexShader = __createGLShader(vertexSource, gl.VERTEX_SHADER); - var fragmentShader = __createGLShader(fragmentSource, gl.FRAGMENT_SHADER); - - program = gl.createProgram(); - - // Fix support for drivers that don't draw if attribute 0 is disabled - for (param in __paramFloat) - { - if (param.name.indexOf("Position") > -1 && StringTools.startsWith(param.name, "openfl_")) - { - gl.bindAttribLocation(program, 0, param.name); - break; - } - } - - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - if (gl.getProgramParameter(program, gl.LINK_STATUS) == 0) - { - var messageBuf = new StringBuf(); - messageBuf.add("Unable to initialize the shader program"); - messageBuf.add("\n"); - messageBuf.add(gl.getProgramInfoLog(program)); - var message = messageBuf.toString(); - Log.error(message); - } - } - catch (error:Dynamic) - { - Logs.traceColored([ - Logs.logText('[Shader] ', BLUE), - Logs.logText('Failed to compile shader ${fileName}: ', RED), - Logs.logText(Std.string(error)) - ], TRACE); - } - return program; - - return program; - } - - var glRawFragmentSource:String; - var glRawVertexSource:String; - - @:noCompletion override private function set_glFragmentSource(value:String):String - { - if(value == null) - value = ShaderTemplates.defaultFragmentSource; - glRawFragmentSource = value; - value = processImports(value, FRAGMENT_SHADER); - value = value.replace("#pragma header", ShaderTemplates.fragHeader).replace("#pragma body", ShaderTemplates.fragBody); - if (value != __glFragmentSource) - { - __glSourceDirty = true; - } - - return __glFragmentSource = value; - } - - @:noCompletion override private function set_glVertexSource(value:String):String - { - if(value == null) - value = ShaderTemplates.defaultVertexSource; - glRawVertexSource = value; - value = processImports(value, VERTEX_SHADER); - - var useBackCompat:Bool = true; - for (regex in ShaderTemplates.vertBackCompatVarList) if (!regex.match(value)) { - useBackCompat = false; - break; - } - - var header = useBackCompat ? ShaderTemplates.vertHeaderBackCompat : ShaderTemplates.vertHeader; - var body = useBackCompat ? ShaderTemplates.vertBodyBackCompat : ShaderTemplates.vertBody; - - value = value.replace("#pragma header", header).replace("#pragma body", body); - - if (value != __glVertexSource) - { - __glSourceDirty = true; - } - - return __glVertexSource = value; + super.__initGL(); } override function __updateGL():Void { @@ -258,276 +174,14 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { super.__updateGL(); } - @:noCompletion private override function __initGL():Void - { - if (__glSourceDirty || __paramBool == null) - { - __glSourceDirty = false; - program = null; - - __inputBitmapData = new Array(); - __paramBool = new Array(); - __paramFloat = new Array(); - __paramInt = new Array(); - - __processGLData(glVertexSource, "attribute"); - __processGLData(glVertexSource, "uniform"); - __processGLData(glFragmentSource, "uniform"); - } - - if (__context != null && program == null) - { - var prefixBuf = new StringBuf(); - prefixBuf.add('#version ${glslVer}\n'); - prefixBuf.add(shaderPrefix); - - var gl = __context.gl; - - prefixBuf.add("#ifdef GL_ES\n"); - if (precisionHint == FULL) { - prefixBuf.add("#ifdef GL_FRAGMENT_PRECISION_HIGH\n"); - prefixBuf.add("precision highp float;\n"); - prefixBuf.add("#else\n"); - prefixBuf.add("precision mediump float;\n"); - prefixBuf.add("#endif\n"); - } else { - prefixBuf.add("precision lowp float;\n"); - } - prefixBuf.add("#endif\n"); - - var prefix = prefixBuf.toString(); - - var vertex = prefix + vertexPrefix + glVertexSource; - var fragment = prefix + fragmentPrefix + glFragmentSource; - - var id = vertex + fragment; - - if (__context.__programs.exists(id)) - { - program = __context.__programs.get(id); - } - else - { - program = __context.createProgram(GLSL); - program.__glProgram = __createGLProgram(vertex, fragment); - __context.__programs.set(id, program); - } - - if (program != null) - { - glProgram = program.__glProgram; - - for (input in __inputBitmapData) { - - if (input.__isUniform) { - input.index = gl.getUniformLocation(glProgram, input.name); - } else { - input.index = gl.getAttribLocation(glProgram, input.name); - } - } - - for (parameter in __paramBool) { - if (parameter.__isUniform) { - parameter.index = gl.getUniformLocation(glProgram, parameter.name); - } else { - parameter.index = gl.getAttribLocation(glProgram, parameter.name); - } - } - - for (parameter in __paramFloat) { - if (parameter.__isUniform) { - parameter.index = gl.getUniformLocation(glProgram, parameter.name); - } else { - parameter.index = gl.getAttribLocation(glProgram, parameter.name); - } - } - - for (parameter in __paramInt) { - if (parameter.__isUniform) { - parameter.index = gl.getUniformLocation(glProgram, parameter.name); - } else { - parameter.index = gl.getAttribLocation(glProgram, parameter.name); - } - } - } - // initInstance(vertex, fragment); // btw make sure to disable the prefixes for ._isInstance - } - } - - var __cancelNextProcessGLData:Bool = false; - @:noCompletion private override function __processGLData(source:String, storageType:String):Void - { - onProcessGLData.dispatch(source, storageType); - if (__cancelNextProcessGLData != (__cancelNextProcessGLData = false)) - return; - var lastMatch = 0, position, regex, name, type; - - if (storageType == "uniform") - { - regex = ~/uniform ([A-Za-z0-9]+) ([A-Za-z0-9_]+)/; - } - else - { - regex = ~/attribute ([A-Za-z0-9]+) ([A-Za-z0-9_]+)/; - } - - while (regex.matchSub(source, lastMatch)) - { - type = regex.matched(1); - name = regex.matched(2); - - if (StringTools.startsWith(name, "gl_")) - { - continue; - } - - var isUniform = (storageType == "uniform"); - registerParameter(name, type, isUniform); - - position = regex.matchedPos(); - lastMatch = position.pos + position.len; - } - } - - function registerParameter(name:String, type:String, isUniform:Bool):Void - { - if (StringTools.startsWith(type, "sampler")) - { - var input = new ShaderInput(); - input.name = name; - input.__isUniform = isUniform; - __inputBitmapData.push(input); - - switch (name) - { - case "openfl_Texture": - __texture = input; - case "bitmap": - __bitmap = input; - default: - } - - Reflect.setField(__data, name, input); - try{Reflect.setField(this, name, input);} catch(e) {} - } - else if (!Reflect.hasField(__data, name) || Reflect.field(__data, name) == null) - { - var parameterType:ShaderParameterType = switch (type) - { - case "bool": BOOL; - case "double", "float": FLOAT; - case "int", "uint": INT; - case "bvec2": BOOL2; - case "bvec3": BOOL3; - case "bvec4": BOOL4; - case "ivec2", "uvec2": INT2; - case "ivec3", "uvec3": INT3; - case "ivec4", "uvec4": INT4; - case "vec2", "dvec2": FLOAT2; - case "vec3", "dvec3": FLOAT3; - case "vec4", "dvec4": FLOAT4; - case "mat2", "mat2x2": MATRIX2X2; - case "mat2x3": MATRIX2X3; - case "mat2x4": MATRIX2X4; - case "mat3x2": MATRIX3X2; - case "mat3", "mat3x3": MATRIX3X3; - case "mat3x4": MATRIX3X4; - case "mat4x2": MATRIX4X2; - case "mat4x3": MATRIX4X3; - case "mat4", "mat4x4": MATRIX4X4; - default: null; - } - - var length = switch (parameterType) - { - case BOOL2, INT2, FLOAT2: 2; - case BOOL3, INT3, FLOAT3: 3; - case BOOL4, INT4, FLOAT4, MATRIX2X2: 4; - case MATRIX3X3: 9; - case MATRIX4X4: 16; - default: 1; - } - - var arrayLength = switch (parameterType) - { - case MATRIX2X2: 2; - case MATRIX3X3: 3; - case MATRIX4X4: 4; - default: 1; - } - - switch (parameterType) - { - case BOOL, BOOL2, BOOL3, BOOL4: - var parameter = new ShaderParameter(); - parameter.name = name; - parameter.type = parameterType; - parameter.__arrayLength = arrayLength; - parameter.__isBool = true; - parameter.__isUniform = isUniform; - parameter.__length = length; - __paramBool.push(parameter); - - if (name == "openfl_HasColorTransform") - { - __hasColorTransform = parameter; - } - - Reflect.setField(__data, name, parameter); - try{Reflect.setField(this, name, parameter);} catch(e) {} - - case INT, INT2, INT3, INT4: - var parameter = new ShaderParameter(); - parameter.name = name; - parameter.type = parameterType; - parameter.__arrayLength = arrayLength; - parameter.__isInt = true; - parameter.__isUniform = isUniform; - parameter.__length = length; - __paramInt.push(parameter); - Reflect.setField(__data, name, parameter); - try{Reflect.setField(this, name, parameter);} catch(e) {} - - default: - var parameter = new ShaderParameter(); - parameter.name = name; - parameter.type = parameterType; - parameter.__arrayLength = arrayLength; - #if lime - if (arrayLength > 0) parameter.__uniformMatrix = new Float32Array(arrayLength * arrayLength); - #end - parameter.__isFloat = true; - parameter.__isUniform = isUniform; - parameter.__length = length; - __paramFloat.push(parameter); - - if (StringTools.startsWith(name, "openfl_")) - { - switch (name) - { - case "openfl_Alpha": __alpha = parameter; - case "openfl_ColorMultiplier": __colorMultiplier = parameter; - case "openfl_ColorOffset": __colorOffset = parameter; - case "openfl_Matrix": __matrix = parameter; - case "openfl_Position": __position = parameter; - case "openfl_TextureCoord": __textureCoord = parameter; - case "openfl_TextureSize": __textureSize = parameter; - default: - } - } - - Reflect.setField(__data, name, parameter); - try{Reflect.setField(this, name, parameter);} catch(e) {} - } - } - } - public function hget(name:String):Dynamic { - if (__instanceFields.contains(name) || __instanceFields.contains('get_${name}')) - return Reflect.getProperty(this, name); - if (!Reflect.hasField(data, name)) - return null; - var field:Dynamic = Reflect.field(data, name); + if (__glSourceDirty || __data == null) __init(); + + if (thisHasField(name) || thisHasField('get_${name}')) return Reflect.getProperty(this, name); + else if (!Reflect.hasField(__data, name)) return null; + + final field:Dynamic = Reflect.field(__data, name); + var cl:String = Type.getClassName(Type.getClass(field)); // little problem we are facing boys... @@ -546,17 +200,19 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { } public function hset(name:String, val:Dynamic):Dynamic { - if (__instanceFields.contains(name) || __instanceFields.contains('set_${name}')) { + if (__glSourceDirty || __data == null) __init(); + + if (thisHasField(name) || thisHasField('set_${name}')) { Reflect.setProperty(this, name, val); return val; } - - if (!Reflect.hasField(data, name)) { - Reflect.setField(data, name, val); + else if (!Reflect.hasField(__data, name)) { + // ??? huh + Reflect.setField(__data, name, val); return val; } - var field = Reflect.field(data, name); + var field = Reflect.field(__data, name); var cl = Type.getClassName(Type.getClass(field)); var isNotNull = val != null; // cant do "field is ShaderInput" for some reason @@ -598,9 +254,133 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { return val; } + + override function __buildSourcePrefix(isFragment:Bool):String { + var result = super.__buildSourcePrefix(isFragment) + '\n$shaderPrefix'; + return isFragment ? result + '\n$fragmentPrefix' : result + '\n$vertexPrefix'; + } + + override function set_glFragmentExtensions(value:Array):Array { + if (value == null) value = __glFragmentExtensionsDefault; + if (value != __glFragmentExtensions) __glSourceDirty = true; + return __glFragmentExtensions = value; + } + + override function set_glVertexExtensions(value:Array):Array { + if (value == null) value = __glVertexExtensionsDefault; + if (value != __glVertexExtensions) __glSourceDirty = true; + return __glVertexExtensions = value; + } + + override function set_glVersion(value:Null):String { + if (value == null || value == "") value = Flags.DEFAULT_GLSL_VERSION; + if ((__glVersionRaw = value) != __glVersion) { + __glSourceDirty = true; + if (__glVertexSourceRaw != null) __glVertexSource = processGLSLText(__glVertexSourceRaw, value, false, __glVertexPragmas); + if (__glFragmentSourceRaw != null) __glFragmentSource = processGLSLText(__glFragmentSourceRaw, value, true, __glFragmentPragmas); + } + + return __glVersion = value; + } + + override function set_glFragmentSource(value:String):String { + if (value == null || value == "") value = __glFragmentSourceDefault; + if ((__glFragmentSourceRaw = value) != null) { + if (__glVersion != (__glVersion = Shader.getGLSLTextVersion(value, __glVersionRaw))) + __glSourceDirty = true; + + value = processGLSLText(value, __glVersion, true, __glFragmentPragmas); + } + + if (value != __glFragmentSource) __glSourceDirty = true; + return __glFragmentSource = value; + } + + override function set_glVertexSource(value:String):String { + if (value == null || value == "") value = __glVertexSourceDefault; + if ((__glVertexSourceRaw = value) != null) { + if (__glVersion != (__glVersion = Shader.getGLSLTextVersion(value, __glVersionRaw))) + __glSourceDirty = true; + + value = processGLSLText(value, __glVersion, false, __glVertexPragmas); + } + + if (value != __glVertexSource) __glSourceDirty = true; + return __glVertexSource = value; + } + + override function set_glFragmentPragmas(value:Map):Map { + if (value == null) value = __glFragmentPragmasDefault; + if (value != __glFragmentPragmas) + __glSourceDirty = true; + + return __glFragmentPragmas = value; + } + + override function set_glVertexPragmas(value:Map):Map { + if (value == null) value = __glVertexPragmasDefault; + if (value != __glVertexPragmas) + __glSourceDirty = true; + + return __glVertexPragmas = value; + } + + function registerParameter(name:String, type:String, isUniform:Bool):Void { + __registerParameter(name, Program3D.getParameterTypeFromGLString(type, 1), 1, -1, isUniform, false, null); + } + + public function toString():String + return FlxStringUtil.getDebugString([for (field in Reflect.fields(data)) LabelValuePair.weak(field, Reflect.field(data, field))]); } class ShaderTemplates { + #if REGION /* Backward Compatibility */ + public static final vertHeader:String = "attribute float openfl_Alpha; +attribute vec4 openfl_ColorMultiplier; +attribute vec4 openfl_ColorOffset; +attribute vec4 openfl_Position; +attribute vec2 openfl_TextureCoord; + +varying float openfl_Alphav; +varying vec4 openfl_ColorMultiplierv; +varying vec4 openfl_ColorOffsetv; +varying vec2 openfl_TextureCoordv; + +uniform mat4 openfl_Matrix; +uniform bool openfl_HasColorTransform; +uniform vec2 openfl_TextureSize; + +attribute float alpha; +attribute vec4 colorMultiplier; +attribute vec4 colorOffset; + +uniform bool hasColorTransform;"; + + public static final vertBody:String = "openfl_TextureCoordv = openfl_TextureCoord; + +if (hasColorTransform) { + openfl_Alphav = openfl_Alpha * colorMultiplier.a; + if (openfl_HasColorTransform) { + openfl_ColorOffsetv = (openfl_ColorOffset / 255.0 * colorMultiplier) + (colorOffset / 255.0); + openfl_ColorMultiplierv = openfl_ColorMultiplier * vec4(colorMultiplier.rgb, 1.0); + } + else { + openfl_ColorOffsetv = colorOffset / 255.0; + openfl_ColorMultiplierv = vec4(colorMultiplier.rgb, 1.0); + } +} +else { + openfl_Alphav = openfl_Alpha * alpha; + if (openfl_HasColorTransform) { + openfl_ColorOffsetv = (openfl_ColorOffset + colorOffset) / 255.0; + openfl_ColorMultiplierv = openfl_ColorMultiplier; + } + else { + openfl_ColorOffsetv = colorOffset / 255.0; + openfl_ColorMultiplierv = vec4(1.0); + } +}"; + public static final fragHeader:String = "varying float openfl_Alphav; varying vec4 openfl_ColorMultiplierv; varying vec4 openfl_ColorOffsetv; @@ -613,31 +393,19 @@ uniform sampler2D bitmap; uniform bool hasTransform; uniform bool hasColorTransform; -vec4 applyFlixelEffects(vec4 color) { - if(!hasTransform) { - return color; - } +vec4 apply_flixel_transform(vec4 color) { + if (!hasTransform) return color; + else if (color.a <= 0.0 || openfl_Alphav == 0.0) return vec4(0.0); - if(color.a == 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - if(!hasColorTransform) { - return color * openfl_Alphav; - } - - color.rgb = color.rgb / color.a; + color.rgb /= color.a; color = clamp(openfl_ColorOffsetv + (color * openfl_ColorMultiplierv), 0.0, 1.0); - - if(color.a > 0.0) { - return vec4(color.rgb * color.a * openfl_Alphav, color.a * openfl_Alphav); - } - return vec4(0.0, 0.0, 0.0, 0.0); + return vec4(color.rgb * color.a * openfl_Alphav, color.a * openfl_Alphav); } +#define applyFlixelEffects(color) apply_flixel_transform(color) + vec4 flixel_texture2D(sampler2D bitmap, vec2 coord) { - vec4 color = texture2D(bitmap, coord); - return applyFlixelEffects(color); + return apply_flixel_transform(texture2D(bitmap, coord)); } uniform vec4 _camSize; @@ -658,53 +426,17 @@ vec4 textureCam(sampler2D bitmap, vec2 pos) { return flixel_texture2D(bitmap, camToOg(pos)); }"; - public static final fragBody:String = "gl_FragColor = flixel_texture2D(bitmap, openfl_TextureCoordv);"; - public static final vertHeader:String = "attribute float openfl_Alpha; -attribute vec4 openfl_ColorMultiplier; -attribute vec4 openfl_ColorOffset; -attribute vec4 openfl_Position; -attribute vec2 openfl_TextureCoord; + public static final fragBody:String = "gl_FragColor = flixel_texture2D(bitmap, openfl_TextureCoordv); +if (gl_FragColor.a == 0.0) discard;"; -varying float openfl_Alphav; -varying vec4 openfl_ColorMultiplierv; -varying vec4 openfl_ColorOffsetv; -varying vec2 openfl_TextureCoordv; + public static final vertBackCompatVarList:Array = [ + ~/attribute float alpha/, + ~/attribute vec4 colorMultiplier/, + ~/attribute vec4 colorOffset/, + ~/uniform bool hasColorTransform/ + ]; -uniform mat4 openfl_Matrix; -uniform bool openfl_HasColorTransform; -uniform vec2 openfl_TextureSize; - -attribute float alpha; -attribute vec4 colorMultiplier; -attribute vec4 colorOffset; -uniform bool hasColorTransform;"; - - public static final vertBody:String = "openfl_Alphav = openfl_Alpha; -openfl_TextureCoordv = openfl_TextureCoord; - -if(openfl_HasColorTransform) { - openfl_ColorMultiplierv = openfl_ColorMultiplier; - openfl_ColorOffsetv = openfl_ColorOffset / 255.0; -} - -openfl_Alphav = openfl_Alpha * alpha; - -if(hasColorTransform) { - openfl_ColorOffsetv = colorOffset / 255.0; - openfl_ColorMultiplierv = colorMultiplier; -} - -gl_Position = openfl_Matrix * openfl_Position;"; - -// TODO: make this ignore comments -public static final vertBackCompatVarList:Array = [ - ~/attribute float alpha/, - ~/attribute vec4 colorMultiplier/, - ~/attribute vec4 colorOffset/, - ~/uniform bool hasColorTransform/ -]; - -public static final vertHeaderBackCompat:String = "attribute float openfl_Alpha; + public static final vertHeaderBackCompat:String = "attribute float openfl_Alpha; attribute vec4 openfl_ColorMultiplier; attribute vec4 openfl_ColorOffset; attribute vec4 openfl_Position; @@ -719,7 +451,7 @@ uniform mat4 openfl_Matrix; uniform bool openfl_HasColorTransform; uniform vec2 openfl_TextureSize;"; -public static final vertBodyBackCompat:String = "openfl_Alphav = openfl_Alpha; + public static final vertBodyBackCompat:String = "openfl_Alphav = openfl_Alpha; openfl_TextureCoordv = openfl_TextureCoord; if(openfl_HasColorTransform) { @@ -728,18 +460,7 @@ if(openfl_HasColorTransform) { } gl_Position = openfl_Matrix * openfl_Position;"; - - public static final defaultVertexSource:String = "#pragma header - -void main(void) { - #pragma body -}"; - - public static final defaultFragmentSource:String = "#pragma header - -void main(void) { - #pragma body -}"; + #end } class ShaderTypeException extends Exception { @@ -753,16 +474,4 @@ class ShaderTypeException extends Exception { this.name = name; super('ShaderTypeException - Tried to set the shader uniform "${name}" as a ${Type.getClassName(has)}, but the shader uniform is a ${Std.string(want)}.'); } -} - -class ShaderErrorPosition { - public var column:Int; - public var line:Int; - public var message:String; - - public function new(line:String, column:String, message:String) { - this.line = Std.parseInt(line); - this.column = Std.parseInt(column); - this.message = message; - } } \ No newline at end of file diff --git a/source/funkin/backend/system/FakeCamera.hx b/source/funkin/backend/system/FakeCamera.hx index 99d0eb1b..31caaac5 100644 --- a/source/funkin/backend/system/FakeCamera.hx +++ b/source/funkin/backend/system/FakeCamera.hx @@ -11,6 +11,11 @@ import openfl.display.BlendMode; import openfl.geom.ColorTransform; import openfl.geom.Point; import openfl.geom.Rectangle; +import openfl.display.TriangleCulling; +import openfl.display3D.Context3DWrapMode; +import openfl.display3D.Context3DCompareMode; +import openfl.filters.BitmapFilter; +import openfl.filters.ShaderFilter; class FakeCamera extends FlxCamera { public static final instance = new FakeCamera(); @@ -21,14 +26,14 @@ class FakeCamera extends FlxCamera { visible = false; } - override function startTrianglesBatch(graphic:FlxGraphic, smoothing:Bool = false, isColored:Bool = false, ?blend:BlendMode, ?hasColorOffsets:Bool, ?shader:FlxShader) { return null;} - override function startQuadBatch(graphic:FlxGraphic, colored:Bool, hasColorOffsets:Bool = false, ?blend:BlendMode, smooth:Bool = false, ?shader:FlxShader) { return null;} + override function startTrianglesBatch(graphic:FlxGraphic, smoothing:Bool = false, isColored:Bool = false, ?blend:BlendMode, ?hasColorOffsets:Bool, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode, ?culling:TriangleCulling) { return null;} + override function startQuadBatch(graphic:FlxGraphic, colored:Bool, hasColorOffsets:Bool = false, ?blend:BlendMode, smooth:Bool = false, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode) { return null;} override function clearDrawStack() {} override function render() {} - public override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {} - public override function copyPixels(?frame:FlxFrame, ?pixels:BitmapData, ?sourceRect:Rectangle, destPoint:Point, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {} - public override function drawTriangles(graphic:FlxGraphic, vertices:DrawData, indices:DrawData, uvtData:DrawData, ?colors:DrawData, ?position:FlxPoint, ?blend:BlendMode, repeat:Bool = false, smoothing:Bool = false, ?transform:ColorTransform, ?shader:FlxShader):Void {} + public override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode) {} + public override function copyPixels(?frame:FlxFrame, ?pixels:BitmapData, ?sourceRect:Rectangle, destPoint:Point, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode) {} + public override function drawTriangles(graphic:FlxGraphic, vertices:DrawData, indices:DrawData, uvtData:DrawData, ?colors:DrawData, ?position:FlxPoint, ?blend:BlendMode, repeat:Bool = false, smoothing:Bool = false, ?transform:ColorTransform, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode, ?culling:TriangleCulling):Void {} public override function update(elapsed:Float) {} @@ -41,10 +46,10 @@ class FakeCallCamera extends FakeCamera { public static final instance = new FakeCallCamera(); public var ignoreDraws:Bool = false; - public dynamic function onDraw(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) { + public dynamic function onDraw(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode) { } - override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) { - if (!ignoreDraws) onDraw(frame, pixels, matrix, transform, blend, smoothing, shader); + override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader, ?wrapMode:Context3DWrapMode, ?depthCompareMode:Context3DCompareMode) { + if (!ignoreDraws) onDraw(frame, pixels, matrix, transform, blend, smoothing, shader, wrapMode, depthCompareMode); } } \ No newline at end of file diff --git a/source/funkin/backend/system/Flags.hx b/source/funkin/backend/system/Flags.hx index df73b06d..03113c0a 100644 --- a/source/funkin/backend/system/Flags.hx +++ b/source/funkin/backend/system/Flags.hx @@ -275,7 +275,7 @@ class Flags { public static var DEFAULT_CHARACTER_GHOSTDISABLE_SOUND:String = "editors/character/ghostDisable"; public static var DEFAULT_CHARACTER_GHOSTENABLE_SOUND:String = "editors/character/ghostEnable"; - public static var DEFAULT_GLSL_VERSION:String = "120"; + public static var DEFAULT_GLSL_VERSION:String = openfl.display.Shader.getDefaultGLVersion(); @:also(funkin.backend.utils.HttpUtil.userAgent) public static var USER_AGENT:String = 'request'; // -- End of Codename's Default Flags -- diff --git a/source/funkin/backend/system/GraphicCacheSprite.hx b/source/funkin/backend/system/GraphicCacheSprite.hx index 6ff61905..cba41b61 100644 --- a/source/funkin/backend/system/GraphicCacheSprite.hx +++ b/source/funkin/backend/system/GraphicCacheSprite.hx @@ -36,7 +36,7 @@ class GraphicCacheSprite extends FlxSprite { if (graphic == null) return; // make their useCount one time higher to prevent them from auto being cleared from cache - graphic.useCount++; + graphic.incrementUseCount(); graphic.destroyOnNoUse = false; cachedGraphics.push(graphic); nonRenderedCachedGraphics.push(graphic); @@ -45,7 +45,7 @@ class GraphicCacheSprite extends FlxSprite { public override function destroy() { for(g in cachedGraphics) { g.destroyOnNoUse = true; - g.useCount--; + g.decrementUseCount(); } graphic = null; super.destroy(); diff --git a/source/funkin/backend/system/Logs.hx b/source/funkin/backend/system/Logs.hx index 1913e4ab..3903caf6 100644 --- a/source/funkin/backend/system/Logs.hx +++ b/source/funkin/backend/system/Logs.hx @@ -115,7 +115,7 @@ final class Logs { Sys.print(t.text); } NativeAPI.setConsoleColors(); - Sys.print("\r\n"); + Sys.println("\r"); __showing = false; #elseif mobile while(__showing) { diff --git a/source/funkin/backend/system/Main.hx b/source/funkin/backend/system/Main.hx index 31d270d9..517ce1f3 100644 --- a/source/funkin/backend/system/Main.hx +++ b/source/funkin/backend/system/Main.hx @@ -58,9 +58,11 @@ class Main extends Sprite // You can pretty much ignore everything from here on - your code should go in your states. public static function preInit() { + #if sys funkin.backend.utils.NativeAPI.registerAsDPICompatible(); funkin.backend.system.CommandLineHandler.parseCommandLine(Sys.args()); funkin.backend.system.Main.fixWorkingDirectory(); + #end } public function new() @@ -134,7 +136,6 @@ class Main extends Sprite FlxG.scaleMode = scaleMode = new FunkinRatioScaleMode(); Conductor.init(); - AudioSwitchFix.init(); EventManager.init(); FlxG.signals.focusGained.add(onFocus); FlxG.signals.preStateSwitch.add(onStateSwitch); @@ -206,16 +207,6 @@ class Main extends Sprite // manual asset clearing since base openfl one does'nt clear lime one // does'nt clear bitmaps since flixel fork does it auto - @:privateAccess { - // clear uint8 pools - for(length=>pool in openfl.display3D.utils.UInt8Buff._pools) { - for(b in pool.clear()) - b.destroy(); - } - - openfl.display3D.utils.UInt8Buff._pools.clear(); - } - MemoryUtil.clearMajor(); } diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 982152d5..d98caf7f 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -40,6 +40,7 @@ class MainState extends FlxState { ControlsUtil.resetCustomControls(); FlxG.bitmap.reset(); FlxG.sound.destroy(true); + FlxG.sound.resetCache(); Paths.assetsTree.reset(); diff --git a/source/funkin/backend/system/OptimizedBitmapData.hx b/source/funkin/backend/system/OptimizedBitmapData.hx index f6cf1117..ecdcd3eb 100644 --- a/source/funkin/backend/system/OptimizedBitmapData.hx +++ b/source/funkin/backend/system/OptimizedBitmapData.hx @@ -4,7 +4,9 @@ import lime.graphics.Image; import lime.graphics.cairo.CairoImageSurface; import openfl.display.BitmapData; import openfl.geom.Rectangle; +import openfl.utils.BitmapDataUtil; +@:deprecated("Use openfl.utils.BitmapDataUtil.toHardware instead.") class OptimizedBitmapData extends BitmapData { @SuppressWarnings("checkstyle:Dynamic") @:noCompletion private override function __fromImage(image:#if lime Image #else Dynamic #end):Void @@ -29,34 +31,8 @@ class OptimizedBitmapData extends BitmapData { __isValid = true; readable = true; - if(FlxG.stage.context3D != null) { - lock(); - getTexture(FlxG.stage.context3D); - getSurface(); - - readable = true; - this.image = null; - - // @:privateAccess - // if (FlxG.bitmap.__doNotDelete) - // MemoryUtil.clearMinor(); - } + BitmapDataUtil.toHardware(this); } #end } - - @SuppressWarnings("checkstyle:Dynamic") - @:dox(hide) public override function getSurface():#if lime CairoImageSurface #else Dynamic #end - { - #if lime - if (__surface == null) - { - __surface = CairoImageSurface.fromImage(image); - } - - return __surface; - #else - return null; - #end - } } \ No newline at end of file diff --git a/source/funkin/backend/system/RotatingSpriteGroup.hx b/source/funkin/backend/system/RotatingSpriteGroup.hx index ad3eea57..520e7741 100644 --- a/source/funkin/backend/system/RotatingSpriteGroup.hx +++ b/source/funkin/backend/system/RotatingSpriteGroup.hx @@ -8,7 +8,12 @@ class RotatingSpriteGroup extends FlxSpriteGroup { if (maxSize <= 0) return super.recycle(ObjectClass, ObjectFactory, Force, Revive); if (group.members.length < maxSize) - return group.recycleCreateObject(ObjectClass, ObjectFactory); + { + if (ObjectFactory != null) return add(ObjectFactory()); + if (ObjectClass != null) return add(Type.createInstance(ObjectClass, [])); + + return null; + } var spr = group.members.shift(); group.members.push(spr); if (Revive) diff --git a/source/funkin/backend/system/macros/Macros.hx b/source/funkin/backend/system/macros/Macros.hx index 2ecbc97f..3bb95cf5 100644 --- a/source/funkin/backend/system/macros/Macros.hx +++ b/source/funkin/backend/system/macros/Macros.hx @@ -58,6 +58,8 @@ class Macros { final macroPath = 'funkin.backend.system.macros.Macros'; Compiler.addMetadata('@:build($macroPath.buildLimeAssetLibrary())', 'lime.utils.AssetLibrary'); + Compiler.addMetadata('@:build($macroPath.buildLimeApplication())', 'lime.app.Application'); + Compiler.addMetadata('@:build($macroPath.buildLimeWindow())', 'lime.ui.Window'); //Adds Compat for #if hscript blocks when you have hscript improved if (Context.defined("hscript_improved") && !Context.defined("hscript")) { @@ -72,5 +74,37 @@ class Macros { return fields; } + + public static function buildLimeApplication():Array { + final fields:Array = Context.getBuildFields(), pos:Position = Context.currentPos(); + for (f in fields) switch (f.kind) { + case FFun(func): switch (f.name) { + case "exec": switch (func.expr.expr) { + case EBlock(exprs): exprs.insert(1, macro funkin.backend.system.Main.preInit()); + default: + } + } + default: + } + + return fields; + } + + public static function buildLimeWindow():Array { + final fields:Array = Context.getBuildFields(), pos:Position = Context.currentPos(); + if (!Context.defined("DARK_MODE_WINDOW")) return fields; + + for (f in fields) switch (f.kind) { + case FFun(func): switch (f.name) { + case "new": switch (func.expr.expr) { + case EBlock(exprs): exprs.push(macro funkin.backend.utils.NativeAPI.setDarkMode(title, true)); + default: + } + } + default: + } + + return fields; + } } #end \ No newline at end of file diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index c6f02cd6..3ad9d02d 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -1,67 +1,94 @@ package funkin.backend.utils; -import flixel.sound.FlxSound; -import lime.media.AudioBuffer; +#if lime_openal +import sys.thread.Mutex; + import lime.utils.ArrayBufferView.ArrayBufferIO; import lime.utils.ArrayBuffer; -#if (lime_cffi && lime_vorbis) -import lime.media.vorbis.Vorbis; -import lime.media.vorbis.VorbisFile; -#end +import flixel.sound.FlxSound; +import flixel.sound.FlxSoundData; -#if (target.threaded) -import sys.thread.Mutex; -#end +typedef ReadCallback = Int->Int->Void; +typedef WindowFunction = Float->Float; -typedef AudioAnalyzerCallback = Int->Int->Void; +final class WindowFunctions { + static inline final TWO_PI:Float = 6.283185307179586; + static inline final FOUR_PI:Float = 12.566370614359172; + static inline final SIX_PI:Float = 18.84955592153876; + static inline final EIGHT_PI:Float = 25.132741228718345; + + public static inline function triangular(x:Float):Float + return 1.0 - Math.abs(x - 0.5) * 2.0; + + public static inline function hann(x:Float):Float + return 0.5 - 0.5 * FlxMath.fastCos(TWO_PI * x); + + public static inline function hamming(x:Float):Float + return 0.53836 - 0.46164 * FlxMath.fastCos(TWO_PI * x); + + public static inline function blackmanNuttall(x:Float):Float + return 0.3635819 - 0.4891775 * FlxMath.fastCos(TWO_PI * x) + 0.1365995 * FlxMath.fastCos(FOUR_PI * x) + - 0.0106411 * FlxMath.fastCos(SIX_PI * x); + + public static inline function blackmanHarris(x:Float):Float + return 0.4243801 - 0.4973406 * FlxMath.fastCos(TWO_PI * x) + 0.0782793 * FlxMath.fastCos(FOUR_PI * x); + + public static inline function flatTop(x:Float):Float + return 0.21557895 - 0.41663158 * FlxMath.fastCos(TWO_PI * x) + 0.277263158 * FlxMath.fastCos(FOUR_PI * x) + + 0.083578947 * FlxMath.fastCos(SIX_PI * x) + 0.006947368 * FlxMath.fastCos(EIGHT_PI * x); +} + +enum abstract TimeUnit(Int) from Int to Int { + var MILLISECOND = 0; + var SECOND = 1; + var SAMPLE = 2; +} /** - * An utility that analyze FlxSounds, + * An utility that analyze FlxSound, * can be used to make waveform or real-time audio visualizer. - * - * FlxSound.amplitude works so if any case if your only checking for peak of current time, use that instead. */ final class AudioAnalyzer { /** * Get bytes from an audio buffer with specified position and wordSize - * @param buffer The audio buffer to get byte from. - * @param position The specified position to get the byte from the audio buffer. - * @param wordSize How many bytes to get with to one byte (Usually it's bitsPerSample / 8 or bitsPerSample >> 3). + * @param buffer The audio buffer to get byte from. + * @param position The specified position to get the byte from the audio buffer. + * @param wordSize How many bytes to get with to one byte (Usually it's bitsPerSample / 8 or bitsPerSample >> 3). * @return Byte from the audio buffer with specified position. */ public static function getByte(buffer:ArrayBuffer, position:Int, wordSize:Int):Int { - if (wordSize == 2) return inline ArrayBufferIO.getInt16(buffer, position); + if (wordSize == 2) return ArrayBufferIO.getInt16(buffer, position); else if (wordSize == 3) { - var b = inline ArrayBufferIO.getUint16(buffer, position) | (buffer.get(position + 2) << 16); - if (b & 0x800000 != 0) return b - 0x1000000; - else return b; + wordSize = ArrayBufferIO.getUint16(buffer, position) | (buffer.get(position + 2) << 16); + if (wordSize & 0x800000 != 0) return wordSize - 0x1000000; + else return wordSize; } - else if (wordSize == 4) return inline ArrayBufferIO.getInt32(buffer, position); - else return inline ArrayBufferIO.getUint8(buffer, position) - 128; + else if (wordSize == 4) return ArrayBufferIO.getInt32(buffer, position); + else return ArrayBufferIO.getInt8(buffer, position); } /** - * Gets levels from the frequencies with specified sample rate. - * @param frequencies Frequencies input. - * @param sampleRate Sample Rate input. - * @param barCount How much bars to get. - * @param levels The output for getting the values, to avoid memory leaks (Optional). - * @param ratio How much ratio for smoothen the values from the previous levels values (Optional, use CoolUtil.getFPSRatio(1 - ratio) to simulate web AnalyserNode.smoothingTimeConstant, 0.35 of smoothingTime works most of the time). - * @param minDb The minimum decibels to cap (Optional, default -63.0, -120 is pure silence). - * @param maxDb The maximum decibels to cap (Optional, default -10.0, Above 0 is not recommended). - * @param minFreq The minimum frequency to cap (Optional, default 20.0, Below 8.0 is not recommended). - * @param maxFreq The maximum frequency to cap (Optional, default 22000.0, Above 23000.0 is not recommended). - * @return Output of levels/bars that ranges from 0 to 1. + * Gets spectrum from the frequencies with specified sample rate. + * @param frequencies Frequencies input. + * @param sampleRate Sample Rate input. + * @param barCount How much bars to get. + * @param spectrum The output for getting the values, to avoid memory leaks (Optional). + * @param ratio How much ratio for smoothen the values from the previous spectrum values (Optional, use FlxMath.getElapsedLerp(1 - ratio) to simulate web AnalyserNode.smoothingTimeConstant, 0.35 of smoothingTime works most of the time). + * @param minDb The minimum decibels to cap (Optional, default -63.0, -120 is pure silence). + * @param maxDb The maximum decibels to cap (Optional, default -10.0, Above 0 is not recommended). + * @param minFreq The minimum frequency to cap (Optional, default 20.0, Below 8.0 is not recommended). + * @param maxFreq The maximum frequency to cap (Optional, default 20000.0, Above 23000.0 is not recommended). + * @return Output of spectrum/bars that ranges from 0 to 1. */ - public static function getLevelsFromFrequencies(frequencies:Array, sampleRate:Int, barCount:Int, ?levels:Array, ratio = 0.0, minDb = -63.0, maxDb = -10.0, minFreq = 20.0, maxFreq = 22000.0):Array { - if (levels == null) levels = []; - levels.resize(barCount); + public static function getSpectrumFromFrequencies(frequencies:Array, sampleRate:Int, barCount:Int, ?spectrum:Array, ratio = 0.0, minDb = -63.0, maxDb = -10.0, minFreq = 20.0, maxFreq = 20000.0):Array { + if (spectrum == null) spectrum = []; + if (spectrum.length != barCount) spectrum.resize(barCount); - var logMin = Math.log(minFreq), logMax = Math.log(maxFreq); - var logRange = logMax - logMin, dbRange = maxDb - minDb, n = frequencies.length; + var logMin = Math.log(minFreq), n = frequencies.length - 1; + var logRange = Math.log(maxFreq) - logMin, dbRangeRate = 1 / (maxDb - minDb), rate = frequencies.length * 2 / sampleRate; inline function calculateScale(i:Int) - return CoolUtil.bound(Math.exp(logMin + (logRange * i / (barCount + 1))) * n * 2 / sampleRate, 0, n - 1); + return FlxMath.bound(Math.exp(logMin + (logRange * i / (barCount + 1))) * rate, 0, n); var s1 = calculateScale(0), s2; var i1 = Math.floor(s1), i2; @@ -81,138 +108,146 @@ final class AudioAnalyzer { } i1 = Math.floor(s1 = s2); - v = CoolUtil.bound(((20 * Math.log(v) / 2.302585092994046) - minDb) / dbRange, 0, 1); - if (ratio > 0 && ratio < 1 && v < levels[i]) levels[i] -= (levels[i] - v) * ratio; - else levels[i] = v; + v = FlxMath.bound((Math.log(v) * 8.685889638065035 - minDb) * dbRangeRate, 0, 1); + if (ratio > 0 && ratio < 1 && v < spectrum[i]) spectrum[i] -= (spectrum[i] - v) * ratio; + else spectrum[i] = v; } - return levels; + return spectrum; } - static var __reverseIndices:Array> = []; - static var __windows:Array> = []; - static var __twiddleReals:Array> = []; - static var __twiddleImags:Array> = []; - static var __freqReals:Array> = []; - static var __freqImags:Array> = []; - static var __freqCalculating:Int = 0; - #if (target.threaded) - static var __mutex:Mutex = new Mutex(); - #end + /** + * Gets levels from the frequencies with specified sample rate. + * @param frequencies Frequencies input. + * @param sampleRate Sample Rate input. + * @param barCount How much bars to get. + * @param levels The output for getting the values, to avoid memory leaks (Optional). + * @param ratio How much ratio for smoothen the values from the previous levels values (Optional, use CoolUtil.getFPSRatio(1 - ratio) to simulate web AnalyserNode.smoothingTimeConstant, 0.35 of smoothingTime works most of the time). + * @param minDb The minimum decibels to cap (Optional, default -63.0, -120 is pure silence). + * @param maxDb The maximum decibels to cap (Optional, default -10.0, Above 0 is not recommended). + * @param minFreq The minimum frequency to cap (Optional, default 20.0, Below 8.0 is not recommended). + * @param maxFreq The maximum frequency to cap (Optional, default 22000.0, Above 23000.0 is not recommended). + * @return Output of levels/bars that ranges from 0 to 1. + * + * deprecated, use getSpectrumFromFrequencies instead. + */ + @:deprecated("Use getSpectrumFromFrequencies instead of getLevelsFromFrequencies.") + public static function getLevelsFromFrequencies(frequencies:Array, sampleRate:Int, barCount:Int, ?levels:Array, ratio = 0.0, minDb = -63.0, maxDb = -10.0, minFreq = 20.0, maxFreq = 22000.0):Array + return inline getSpectrumFromFrequencies(frequencies, sampleRate, barCount, levels, ratio, minDb, maxDb, minFreq, maxFreq); + + static final _permutations:Map> = []; + static final _twiddleReals:Map> = []; + static final _twiddleImags:Map> = []; + static final _reals:Array> = []; + static final _imags:Array> = []; + static var _freqCalculating:Int = 0; + static final _mutex = new Mutex(); /** * Gets frequencies from the samples. - * @param samples The samples (can be from AudioAnalyzer.getSamples). - * @param fftN How much samples for the fft to get, Has to be power of two, or it won't work. - * @param useWindowing Should fft related stuff use blackman windowing? (Web AnalyzerNode windowing), Most of the time it's not worth it. - * @param frequencies The output for getting the frequencies, to avoid memory leaks (Optional). - * @return Output of frequencies. + * @param samples The samples (can be from FunkinAudioAnalyzer.getSamples). + * @param window The windowing function to use when passed. + * @param frequencies The output for getting the frequencies, to avoid memory leaks (Optional). + * @return Output of frequencies. */ - public static function getFrequenciesFromSamples(samples:Array, fftN = 2048, useWindowing = false, ?frequencies:Array):Array { - var log = Math.floor(Math.log(fftN) / 0.6931471805599453); - if (log == 0) throw "AudioAnalyzer.getFrequenciesFromSamples: Cannot insert a fftN of 1"; + public static function getFrequenciesFromSamples(samples:Array, ?window:WindowFunction, ?frequencies:Array, ?fftN:Int):Array { + if (fftN == null) fftN = samples.length; - var i = log - 1; - fftN = 1 << log; + var bits = 0; + while ((fftN >>= 1) > 0) bits++; + if (bits == 0) throw "FunkinAudioAnalyzer.getFrequenciesFromSamples: Cannot insert a sample length or fftN of 1"; - #if (target.threaded) __mutex.acquire(); #end - var reals:Array = __freqReals[__freqCalculating], imags:Array = __freqImags[__freqCalculating]; - if (reals == null) { - __freqReals.push(reals = []); - __freqImags.push(imags = []); + fftN = 1 << bits; + var fftN2 = fftN >> 1, n = fftN - 1; + + var permutation:Array, twiddleReal:Array, twiddleImag:Array; + _mutex.acquire(); + + var real:Array = _reals[_freqCalculating], imag:Array = _imags[_freqCalculating]; + if (real == null) { + _reals.push(real = []); + _imags.push(imag = []); } - __freqCalculating++; + _freqCalculating++; - var reverseIndices:Array = __reverseIndices[i]; - var windows:Array = __windows[i]; - var twiddleReals:Array = __twiddleReals[i]; - var twiddleImags:Array = __twiddleImags[i]; + if (_permutations.exists(bits)) { + permutation = _permutations.get(bits); + twiddleReal = _twiddleReals.get(bits); + twiddleImag = _twiddleImags.get(bits); + } + else { + (permutation = []).resize(fftN); + (twiddleReal = []).resize(fftN2); + (twiddleImag = []).resize(fftN2); - if (reverseIndices == null) { - __reverseIndices.resize(log); - __windows.resize(log); - __twiddleReals.resize(log); - __twiddleImags.resize(log); - - (reverseIndices = []).resize(fftN); - (windows = []).resize(fftN); - (twiddleReals = []).resize(fftN); - (twiddleImags = []).resize(fftN); - - var f; + var ang:Float; for (i in 0...fftN) { - f = 2 * Math.PI * (i / fftN); - windows[i] = 0.42 - 0.5 * Math.cos(f) + 0.08 * Math.cos(2 * f); - reverseIndices[i] = __bitReverse(i, log); - twiddleReals[i] = Math.cos(-f); - twiddleImags[i] = Math.sin(-f); + permutation[i] = _bitReverse(i, bits); + if (i < fftN2) { + twiddleReal[i] = Math.cos((ang = -6.283185307179586 * i / n)); + twiddleImag[i] = Math.sin(ang); + } } - __reverseIndices[i] = reverseIndices; - __windows[i] = windows; - __twiddleReals[i] = twiddleReals; - __twiddleImags[i] = twiddleImags; + _permutations.set(bits, permutation); + _twiddleReals.set(bits, twiddleReal); + _twiddleImags.set(bits, twiddleImag); } - #if (target.threaded) __mutex.release(); #end + _mutex.release(); - if (fftN > reals.length) { - reals.resize(fftN); - imags.resize(fftN); + if (fftN > real.length) { + real.resize(fftN); + imag.resize(fftN); } if (frequencies == null) frequencies = []; - frequencies.resize(1 << i); + if (frequencies.length != fftN2) frequencies.resize(fftN2); - i = samples.length; - while (i > 0) { - i--; - if (useWindowing) reals[reverseIndices[i]] = samples[i] * windows[i]; - else reals[reverseIndices[i]] = samples[i]; - imags[i] = 0; + var tr = 1 / n; + for (i in 0...fftN) { + real[permutation[i]] = samples[i]; + if (window != null) real[permutation[i]] *= window(i * tr); + imag[i] = 0; } - var size = 1, n = fftN, half = 1, k, i0, i1, t, tr:Float, ti:Float; - while ((size <<= 1) < fftN) { - n >>= 1; - i = 0; - while (i < fftN) { - k = 0; - while (k < half) { - i1 = (i0 = i + k) + half; - t = (k * n) % fftN; - - tr = reals[i1] * twiddleReals[t] - imags[i1] * twiddleImags[t]; - ti = reals[i1] * twiddleImags[t] + imags[i1] * twiddleReals[t]; - reals[i1] = reals[i0] - tr; - imags[i1] = imags[i0] - ti; - reals[i0] += tr; - imags[i0] += ti; - - k++; + var half = 1, g:Int, b:Int, r:Int, i0:Int, i1:Int, ti:Float; + while (fftN2 > 0) { + g = 0; + while (g < fftN) { + b = r = 0; + while (b < half) { + i1 = (i0 = g + b) + half; + tr = real[i1] * twiddleReal[r] - imag[i1] * twiddleImag[r]; + ti = real[i1] * twiddleImag[r] + imag[i1] * twiddleReal[r]; + real[i1] = real[i0] - tr; + imag[i1] = imag[i0] - ti; + real[i0] += tr; + imag[i0] += ti; + b++; + r += fftN2; } - i += size; + g += half << 1; } - half = size; + half <<= 1; + fftN2 >>= 1; } tr = 1.0 / fftN; - i = 1 << (log - 1); - while (i > 1) { - i--; - frequencies[i] = 2 * Math.sqrt(reals[i] * reals[i] + imags[i] * imags[i]) * tr; - } - frequencies[0] = Math.sqrt(reals[0] * reals[0] + imags[0] * imags[0]) * tr; + i0 = frequencies.length - 1; + for (i in 1...i0) frequencies[i] = 2 * Math.sqrt(real[i] * real[i] + imag[i] * imag[i]) * tr; + frequencies[0] = Math.sqrt(real[0] * real[0] + imag[0] * imag[0]) * tr; + frequencies[i0] = Math.sqrt(real[i0] * real[i0] + imag[i0] * imag[i0]) * tr; - #if (target.threaded) __mutex.acquire(); #end - __freqCalculating--; - #if (target.threaded) __mutex.release(); #end + _mutex.acquire(); + _freqCalculating--; + _mutex.release(); return frequencies; } - static function __bitReverse(x:Int, log:Int):Int { - var y = 0, i = log; + static function _bitReverse(x:Int, bits:Int):Int { + var y = 0, i = bits; while (i > 0) { y = (y << 1) | (x & 1); x >>= 1; @@ -227,346 +262,357 @@ final class AudioAnalyzer { public var sound:FlxSound; /** - * How much samples for the fft to get. - * Usually for getting the levels or frequencies of the sound. - * + * The current data from sound. + */ + public var data(default, null):FlxSoundData; + + /** + * How much samples for the fourier transform to get. * Has to be power of two, or it won't work. */ public var fftN:Int; - /** - * Should fft related stuff use blackman windowing? (Web AnalyzerNode windowing). - * Most of the time looks bad with this. - */ - public var useWindowingFFT:Bool; - - /** - * The current buffer from sound. - */ - public var buffer(default, null):AudioBuffer; - /** * The current byteSize from buffer. - * Example the byteSize of 16 BitsPerSample is 32768 (1 << 16-1) + * Example the byteSize of 16 BitsPerSample is 32768 (1 << (16 - 1)) */ public var byteSize(default, null):Int; - var __toBits:Float; - var __wordSize:Int; - var __sampleSize:Int; + var _sampleSize:Int; + var _mins:Array = []; + var _maxs:Array = []; + //var _decoder:FunkinAudioDecoder; + //var _buffer:ArrayBuffer; + //var _bufferLen:Int; + //var _bufferLastSize:Int; + //var _bufferLastSample:Int; + var _sampleIndex:Int; + var _sampleChannel:Int; + var _sampleValue:Int; + var _sampleValueGain:Float; + var _sampleOutputMerge:Bool; + var _sampleOutputLength:Int; + var _sampleOutput:Array; + var _freqSamples:Array; + var _frequencies:Array; - #if (lime_cffi && lime_vorbis) - var __vorbis:VorbisFile; - var __buffer:ArrayBuffer; - var __bufferSize:Int; - var __bufferLastSize:Int; - var __bufferTime:Float; - var __bufferLastTime:Float; - #end - - // analyze - var __min:Array = []; - var __max:Array = []; - var __minByte:Int; - var __maxByte:Int; - - // samples - var __sampleIndex:Int; - var __sampleChannel:Int; - var __sampleToValue:Float; - var __sampleOutputMerge:Bool; - var __sampleOutputLength:Int; - var __sampleOutput:Array; - - // frequencies - var __freqSamples:Array; - var __frequencies:Array; - - /** - * Creates an analyzer for specified FlxSound - * @param sound An FlxSound to analyze. - * @param fftN How much samples for fft to get (Optional, default 2048, 4096 is recommended for highest quality). - * @param useWindowingFFT Should fft related stuff use blackman windowing? (Web AnalyzerNode windowing). - */ - public function new(sound:FlxSound, fftN = 2048, useWindowingFFT = false) { + public function new(sound:FlxSound, fftN = 4096) { this.sound = sound; this.fftN = fftN; - this.useWindowingFFT = useWindowingFFT; - __check(); + _check(); } - function __check() if (sound != null && sound.buffer != buffer) { - byteSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1); + function _check() { + if (sound != null && !sound.data.isDestroyed) { + if (sound.data != data) + { + byteSize = 1 << ((data = sound.data).bitsPerSample - 1); + _sampleSize = data.channels * (data.bitsPerSample >> 3); + _mins.resize(data.channels); + _maxs.resize(data.channels); + //_decoder?.destroy(); + } + } + else data = null; + } - #if (lime_cffi && lime_vorbis) - __vorbis = null; - __bufferLastSize = 0; - __bufferTime = Math.NaN; - __bufferLastTime = Math.NaN; - #end - - __toBits = buffer.sampleRate / 1000 * (__sampleSize = buffer.channels * (__wordSize = buffer.bitsPerSample >> 3)); - __min.resize(buffer.channels); - __max.resize(buffer.channels); + /** + * Gets spectrum from an attached sound from position. + * @param pos Position to get (Optional). + * @param timeUnit TimeUnit to use for positions (Optional). + * @param gain How much gain multiplier will it affect the output. (Optional, default 1.0). + * @param barCount How much bars to get. + * @param spectrum The output for getting the values, to avoid memory leaks (Optional). + * @param ratio How much ratio for smoothen the values from the previous spectrum values (Optional, use FlxMath.getElapsedLerp(1 - ratio) to simulate web AnalyserNode.smoothingTimeConstant, 0.35 of smoothingTime works most of the time). + * @param minDb The minimum decibels to cap (Optional, default -63.0, -120 is pure silence). + * @param maxDb The maximum decibels to cap (Optional, default -10.0, Above 0 is not recommended). + * @param minFreq The minimum frequency to cap (Optional, default 20.0, Below 8.0 is not recommended). + * @param maxFreq The maximum frequency to cap (Optional, default 20000.0, Above 23000.0 is not recommended). + * @return Output of spectrum/bars that ranges from 0 to 1. + */ + public function getSpectrum(?pos:Float, ?timeUnit:TimeUnit, ?gain:Float, ?window:WindowFunction, barCount:Int, ?spectrum:Array, ?ratio:Float, ?minDb:Float, ?maxDb:Float, ?minFreq:Float, ?maxFreq:Float):Array { + return getSpectrumFromFrequencies(_frequencies = getFrequencies(pos, timeUnit, gain, window, _frequencies), data.sampleRate, barCount, spectrum, ratio, minDb, maxDb, minFreq, maxFreq); } /** * Gets levels from an attached FlxSound from startPos, basically a minimized of frequencies. - * @param startPos Start Position to get from sound in milliseconds. - * @param volume How much volume multiplier will it affect the output. (Optional, default 1.0). - * @param barCount How much bars to get. - * @param levels The output for getting the values, to avoid memory leaks (Optional). - * @param ratio How much ratio for smoothen the values from the previous levels values (Optional, use CoolUtil.getFPSRatio(1 - ratio) to simulate web AnalyserNode.smoothingTimeConstant, 0.35 of smoothingTime works most of the time). - * @param minDb The minimum decibels to cap (Optional, default -63.0, -120 is pure silence). - * @param maxDb The maximum decibels to cap (Optional, default -10.0, Above 0 is not recommended). - * @param minFreq The minimum frequency to cap (Optional, default 20.0, Below 8.0 is not recommended). - * @param maxFreq The maximum frequency to cap (Optional, default 22000.0, Above 23000.0 is not recommended). - * @return Output of levels/bars that ranges from 0 to 1. + * @param startPos Start Position to get from sound in milliseconds. + * @param volume How much volume multiplier will it affect the output. (Optional, default 1.0). + * @param barCount How much bars to get. + * @param levels The output for getting the values, to avoid memory leaks (Optional). + * @param ratio How much ratio for smoothen the values from the previous levels values (Optional, use CoolUtil.getFPSRatio(1 - ratio) to simulate web AnalyserNode.smoothingTimeConstant, 0.35 of smoothingTime works most of the time). + * @param minDb The minimum decibels to cap (Optional, default -63.0, -120 is pure silence). + * @param maxDb The maximum decibels to cap (Optional, default -10.0, Above 0 is not recommended). + * @param minFreq The minimum frequency to cap (Optional, default 20.0, Below 8.0 is not recommended). + * @param maxFreq The maximum frequency to cap (Optional, default 22000.0, Above 23000.0 is not recommended). + * @return Output of levels/bars that ranges from 0 to 1. + * + * deprecated, use getLevels instead. */ + @:deprecated("Use getSpectrum instead of getLevels.") public function getLevels(?startPos:Float, ?volume:Float, barCount:Int, ?levels:Array, ?ratio:Float, ?minDb:Float, ?maxDb:Float, ?minFreq:Float, ?maxFreq:Float):Array - return inline getLevelsFromFrequencies(__frequencies = getFrequencies(startPos, volume, __frequencies), buffer.sampleRate, barCount, levels, ratio, minDb, maxDb, minFreq, maxFreq); + return inline getSpectrum(startPos, MILLISECOND, volume, null, barCount, levels, ratio, minDb, maxDb, minFreq, maxFreq); /** - * Gets frequencies from an attached FlxSound from startPos. - * @param startPos Start Position to get from sound in milliseconds. - * @param volume How much volume multiplier will it affect the output. (Optional, default 1.0). - * @param frequencies The output for getting the frequencies, to avoid memory leaks (Optional). - * @return Output of frequencies. + * Gets frequencies from an attached sound from position. + * @param pos Position to get. (Optional). + * @param timeUnit TimeUnit to use for positions. (Optional). + * @param gain How much gain multiplier will it affect the output. (Optional, default 1.0). + * @param window The windowing function to use when passed. + * @param frequencies The output for getting the frequencies, to avoid memory leaks (Optional). + * @return Output of frequencies. */ - public function getFrequencies(?startPos:Float, ?volume:Float, ?frequencies:Array):Array - return inline getFrequenciesFromSamples(__freqSamples = getSamples(startPos != null ? startPos : sound.time, fftN, true, -1, volume, __freqSamples), fftN, useWindowingFFT, frequencies); - - /** - * Analyzes an attached FlxSound from startPos to endPos in milliseconds to get the amplitudes. - * @param startPos Start Position to get from sound in milliseconds. - * @param endPos End Position to get from sound in milliseconds. - * @param outOrOutMin The output minimum value from the analyzer, indices is in channels (0 to -0.5 -> 0 to 0.5) (Optional, if outMax doesn't get passed in, it will be [min, max] with all channels combined instead). - * @param outMax The output maximum value from the analyzer, indices is in channels (Optional). - * @return Output of amplitude from given position. - */ - public function analyze(startPos:Float, endPos:Float, ?outOrOutMin:Array, ?outMax:Array):Float { - var hasOut = outOrOutMin != null; - var hasTwoOut = hasOut && outMax != null; - - if (hasTwoOut) for (i in 0...buffer.channels) __min[i] = __max[i] = 0; - __minByte = __maxByte = 0; - - __check(); - __read(startPos, endPos, hasTwoOut ? __analyzeCallback : __analyzeCallbackSimple); - - if (hasOut) { - var f:Float; - if (hasTwoOut) for (i in 0...buffer.channels) { - if (outOrOutMin[i] < (f = __min[i] / byteSize)) outOrOutMin[i] = f; - if (outMax[i] < (f = __max[i] / byteSize)) outMax[i] = f; - } - else { - outOrOutMin.resize(2); - if (outOrOutMin[0] < (f = __minByte / byteSize)) outOrOutMin[0] = f; - if (outOrOutMin[1] < (f = __maxByte / byteSize)) outOrOutMin[1] = f; - } + public function getFrequencies(?pos:Float, ?timeUnit:TimeUnit, ?gain:Float, ?window:WindowFunction, ?frequencies:Array):Array { + if (pos == null) { + if (sound == null) return frequencies; + _check(); + if ((pos = sound.time / 1000 * data.sampleRate - fftN) < 0) pos = 0; + timeUnit = SAMPLE; } - - return (__maxByte + __minByte) / byteSize; + return getFrequenciesFromSamples(_freqSamples = getSamples(pos, timeUnit, fftN, true, -1, gain, _freqSamples), window, frequencies); } - function __analyzeCallback(b:Int, c:Int):Void - ((b > __max[c]) ? (if ((__max[c] = b) > __maxByte) (__maxByte = b)) : (if (-b > __min[c]) (if ((__min[c] = -b) > __minByte) (__minByte = __min[c])))); + /** + * Analyzes an attached sound from startPos to endPos in milliseconds to get the amplitudes. + * @param startPos Start Position to get. + * @param endPos End Position to get. + * @param timeUnit TimeUnit to use for positions. + * @param outOrOutMins The output minimum value from the analyzer, indices is in channels (0 to -0.5 -> 0 to 0.5) (Optional, if outMax doesn't get passed in, it will be [min, max] with all channels combined instead). + * @param outMaxs The output maximum value from the analyzer, indices is in channels (Optional). + * @return Output of amplitude from given position. + */ + public function analyze(startPos:Float, endPos:Float, ?timeUnit:TimeUnit, ?outOrOutMins:Array, ?outMaxs:Array):Float { + var hasOut = outOrOutMins != null; + var hasTwoOut = hasOut && outMaxs != null; - function __analyzeCallbackSimple(b:Int, c:Int):Void - ((b > __maxByte) ? (__maxByte = b) : (if (-b > __minByte) (__minByte = -b))); + _check(); + var conversion:Float = switch (timeUnit) { + case SAMPLE: 1; + case SECOND: data.sampleRate; + default: data.sampleRate / 1000; + } + for (i in 0...data.channels) _mins[i] = _maxs[i] = -0x7FFFFFFF; + if (startPos > endPos) _read(Math.floor(startPos * conversion), Math.floor(endPos * conversion), _analyzeRead); + + var min = -0x7FFFFFFF, max = -0x7FFFFFFF, v = 1 / byteSize, f:Float; + for (i in 0...data.channels) { + if (hasTwoOut) { + if ((f = _mins[i] * v) > outOrOutMins[i]) outOrOutMins[i] = f; + if ((f = _maxs[i] * v) > outMaxs[i]) outMaxs[i] = f; + } + if (_maxs[i] > max) max = _maxs[i]; + if (_mins[i] > min) min = _mins[i]; + } + + if (hasOut && outMaxs == null) { + if ((f = min * v) > outOrOutMins[0]) outOrOutMins[0] = f; + if ((f = max * v) > outOrOutMins[1]) outOrOutMins[1] = f; + } + return (max + min) * v; + } + + function _analyzeRead(b:Int, c:Int) ((b > _maxs[c]) ? (_maxs[c] = b) : (if (-b > _mins[c]) (_mins[c] = -b))); /** * Gets samples from startPos with given length of samples. - * @param startPos Start Position to get from sound in milliseconds. - * @param length Length of Samples. - * @param mono Merge all of the byte channels of samples in one channel instead (Optional). - * @param channel What channels to get from? (-1 == All Channels, Optional, this will be ignored if mono is enabled). - * @param volume How much volume multiplier will it affect the output. (Optional, default 1.0). - * @param output An Output that gets passed into this function, usually for to avoid memory leaks (Optional). - * @param outputMerge Merge with previous values (Optional, default false). - * @return Output of samples. + * @param startPos Start Position to get. + * @param timeUnit TimeUnit to use for positions. + * @param length Length of Samples. + * @param mono Merge all of the byte channels of samples in one channel instead (Optional). + * @param channel What channels to get from? (-1 == All Channels, Optional, this will be ignored if mono is enabled). + * @param gain How much gain multiplier will it affect the output. (Optional, default 1.0). + * @param output An Output that gets passed into this function, usually for to avoid memory leaks (Optional). + * @param outputMerge Merge with previous values (Optional, default false). + * @return Output of samples. */ - public function getSamples(startPos:Float, length:Int, mono = true, channel = -1, volume = 1.0, ?output:Array, ?outputMerge = false):Array { - ((!mono && (__sampleChannel = channel) == -1) ? (__sampleOutputLength = length * buffer.channels) : (__sampleOutputLength = length)); - ((output == null) ? (__sampleOutput = output = []) : (__sampleOutput = output)).resize(__sampleOutputLength); - ((mono) ? (__sampleToValue = volume / (byteSize * buffer.channels)) : (__sampleToValue = 1.0 / byteSize)); - __sampleOutputMerge = outputMerge; - __sampleIndex = 0; + public function getSamples(startPos:Float, ?timeUnit:TimeUnit, length:Int, mono = true, channel = -1, gain = 1.0, ?output:Array, ?outputMerge = false):Array { + _check(); + ((!mono && channel == -1) ? (_sampleOutputLength = length * data.channels) : (_sampleOutputLength = length)); + if (((output == null) ? (_sampleOutput = output = []) : (_sampleOutput = output)).length != _sampleOutputLength) output.resize(_sampleOutputLength); + _sampleValueGain = gain; + _sampleOutputMerge = outputMerge; + _sampleIndex = 0; + _sampleValue = 0; - __check(); - __read(startPos, startPos + (length / __toBits * buffer.channels), mono ? __getSamplesCallbackMono : (channel == -1 ? __getSamplesCallback : __getSamplesCallbackChannel)); + final samplePos = Math.floor(switch (timeUnit) { + case SAMPLE: startPos; + case SECOND: startPos * data.sampleRate; + default: startPos * data.sampleRate / 1000; + }); + _sampleChannel = mono ? data.channels - 1 : channel; + if (length > 0) _read(samplePos, samplePos + length, mono ? _getSamplesCallbackMono : (channel == -1 ? _getSamplesCallback : _getSamplesCallbackChannel)); - __sampleOutput = null; + _sampleOutput = null; return output; } - function __getSamplesCallbackMono(b:Int, c:Int):Void if (__sampleIndex < __sampleOutputLength) { - if (c == 0) { - if (__sampleOutputMerge) __sampleOutput[__sampleIndex] += b * __sampleToValue; - else __sampleOutput[__sampleIndex] = b * __sampleToValue; - } - else if (c == buffer.channels) { - __sampleOutput[__sampleIndex] += b * __sampleToValue; - __sampleIndex++; - } - else - __sampleOutput[__sampleIndex] += b * __sampleToValue; - } + function _getSamplesCallbackMono(b:Int, c:Int):Void if (_sampleIndex < _sampleOutputLength) { + if (c == 0) _sampleValue = idiv(b, data.channels); + else _sampleValue += idiv(b, data.channels); - function __getSamplesCallbackChannel(b:Int, c:Int):Void if (__sampleIndex < __sampleOutputLength) { - if (c == __sampleChannel) { - if (__sampleOutputMerge) __sampleOutput[__sampleIndex] += b * __sampleToValue; - else __sampleOutput[__sampleIndex] = b * __sampleToValue; - __sampleIndex++; + if (c == _sampleChannel) { + if (_sampleOutputMerge) _sampleOutput[_sampleIndex] += _sampleValue / byteSize; + else _sampleOutput[_sampleIndex] = _sampleValue / byteSize; + _sampleIndex++; } } - function __getSamplesCallback(b:Int, c:Int):Void if (__sampleIndex < __sampleOutputLength) { - if (__sampleOutputMerge) __sampleOutput[__sampleIndex] += b * __sampleToValue; - else __sampleOutput[__sampleIndex] = b * __sampleToValue; - __sampleIndex++; + function _getSamplesCallbackChannel(b:Int, c:Int):Void if (_sampleIndex < _sampleOutputLength) { + if (c == _sampleChannel) { + if (_sampleOutputMerge) _sampleOutput[_sampleIndex] += b / byteSize; + else _sampleOutput[_sampleIndex] = b / byteSize; + _sampleIndex++; + } + } + + function _getSamplesCallback(b:Int, c:Int):Void if (_sampleIndex < _sampleOutputLength) { + if (_sampleOutputMerge) _sampleOutput[_sampleIndex] += b / byteSize; + else _sampleOutput[_sampleIndex] = b / byteSize; + _sampleIndex++; } /** - * Read an attached FlxSound from startPos to endPos in milliseconds with a callback. - * @param startPos Start Position to get from sound in milliseconds. - * @param endPos End Position to get from sound in milliseconds. - * @param callback Int->Int->Void Byte->Channels->Void Callback to get the byte of a sample. + * Read an attached sound from startPos to endPos in milliseconds with a callback. + * @param startPos Start Position to get. + * @param endPos End Position to get. + * @param timeUnitTimeUnit to use for positions. + * @param callback Byte:Int->Channels:Int->Void Callback to get the byte of a sample. */ - public function read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { - __check(); - __read(startPos, endPos, callback); + public function read(startPos:Float, endPos:Float, ?timeUnit:TimeUnit, callback:ReadCallback) { + _check(); + var conversion:Float = switch (timeUnit) { + case SAMPLE: 1; + case SECOND: data.sampleRate; + default: data.sampleRate / 1000; + } + if (startPos > endPos) _read(Math.floor(startPos * conversion), Math.floor(endPos * conversion), callback); } - inline function __read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { - if (buffer.data != null) __readData(startPos, endPos, callback); - #if lime_cffi - else if (__canReadStream() && (startPos += __readStream(startPos, endPos, callback)) >= endPos) {} - #if lime_vorbis - else if (__prepareDecoder()) __readDecoder(startPos, endPos, callback); - #end - #end + function _read(startSample:Int, endSample:Int, callback:ReadCallback) { + // use data in ram if available + if (data.buffer.data != null) _readData(startSample * _sampleSize, endSample * _sampleSize, callback); + // use decoded datas that have been used in streaming sound to reduce jumping disk seeking + // if not use decoder and use seeking instead* + else if (sound.loaded) _readStream(startSample, endSample, callback); + + // TODO + //else if ((!sound.loaded || (startSample = _readStream(startSample, endSample, callback)) < endSample) && _prepareDecoder()) + // _readDecoder(startSample, endSample, callback); } - inline function __readData(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { - var pos = Math.floor(startPos * __toBits), end = Math.min(Math.floor(endPos * __toBits), buffer.data.buffer.length), c = 0; - pos -= pos % __sampleSize; - end -= end % __sampleSize; - - while (pos < end) { - callback(getByte(buffer.data.buffer, pos, __wordSize), c); - if (++c > buffer.channels) c = 0; - pos += __wordSize; + inline function _readData(startIndex:Int, endIndex:Int, callback:ReadCallback) { + if (endIndex > data.buffer.data.byteLength) endIndex = data.buffer.data.byteLength; + var buffer = data.buffer.data.buffer, byteRate = data.bitsPerSample >> 3, c = 0; + while (startIndex < endIndex) { + callback(getByte(buffer, startIndex, byteRate), c); + startIndex += byteRate; + if (++c == data.channels) c = 0; } } - #if lime_cffi - inline function __canReadStream():Bool - @:privateAccess return sound._source != null && sound._source.__backend != null && sound._source.__backend.playing; + function _readStream(startSample:Int, endSample:Int, callback:ReadCallback):Int @:privateAccess { + final backend = sound.source.__backend; + if (backend.filledBuffers == 0) return startSample; - inline function __readStream(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback):Float @:privateAccess { - final backend = sound._source.__backend; + backend.mutex.acquire(); - // TODO: Wrap it with try until i figured it out an effective way to do this... - // So... sometimes it just uses the decoder even if it looks good?? please help - var n = Math.floor((endPos - startPos) * __toBits); - var i = backend.bufferLengths.length - backend.requestBuffers - 1, time:Float; - while (++i < backend.bufferLengths.length) if (startPos >= (time = backend.bufferTimes[i] * 1000)) { - var pos = Math.floor((startPos - time) * __toBits), buf = backend.bufferDatas[i].buffer, size = backend.bufferLengths[i], c = 0; - while (pos >= size) { - if (++i >= backend.bufferLengths.length) break; - pos -= size; - buf = backend.bufferDatas[i].buffer; - size = backend.bufferLengths[i]; - } - if (i >= backend.bufferLengths.length) break; - if ((pos -= pos % __sampleSize) < 0) pos = 0; - n -= pos % __sampleSize; + final max = backend.bufferViews.length; + var byteRate = data.bitsPerSample >> 3, i = max - backend.queuedBuffers, buffer:ArrayBuffer, bufferLen:Int, bufferSample:Int, pos:Int, c:Int; - while (n > 0) { - callback(getByte(buf, pos, __wordSize), c); - if (++c > buffer.channels) c = 0; - if ((pos += __wordSize) >= size) { - if (++i >= backend.bufferLengths.length) break; - pos = 0; - buf = backend.bufferDatas[i].buffer; - size = backend.bufferLengths[i]; - } - n -= __wordSize; - } - - break; - } - - return endPos - (n / __toBits); - } - - #if lime_vorbis - inline function __prepareDecoder():Bool @:privateAccess { - if (buffer.__srcVorbisFile == null) return __vorbis != null; - if (__vorbis != null) return true; - if ((__vorbis = buffer.__srcVorbisFile.clone()) != null) { // IM HOPING IT HAVE A GC CLOSURE. - __buffer = new ArrayBuffer(__bufferSize = (buffer.sampleRate >> 1) * __sampleSize); // 0.5 seconds of buffers. - return true; - } - return false; - } - - inline function __readDecoder(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { - var n = Math.floor((endPos - startPos) * __toBits); - if ((n -= n % __sampleSize) > 0) { - var pos = Math.floor((startPos - __bufferTime * 1000) * __toBits); - pos -= pos % __sampleSize; - - var doRead = __bufferLastSize == 0 || (pos < 0 && pos >= __bufferSize); - if (doRead) { - if (startPos < 1) { - __vorbis.rawSeek(0); - __bufferTime = 0; - } - else - __vorbis.timeSeek(__bufferTime = startPos / 1000); - - __bufferLastSize = pos = 0; - } - - var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, ranOut = false, c = 0, result; - while (true) { - if (doRead) { - result = __vorbis.read(__buffer, pos, __bufferSize - pos, isBigEndian, __wordSize, true); - if (result == Vorbis.HOLE) continue; - else if (result < 0) break; - else if (!(ranOut = result == 0)) { - __bufferLastTime = __vorbis.timeTell(); - __bufferLastSize += result; - while (pos < __bufferLastSize) { - callback(getByte(__buffer, pos, __wordSize), c); - if (++c > buffer.channels) c = 0; - pos += __wordSize; - if ((n -= __wordSize) <= 0) break; + while (i < max && startSample < endSample) { + if (startSample >= (bufferSample = backend.bufferCurs[i])) { + if ((pos = (startSample - bufferSample) * _sampleSize) < (bufferLen = backend.bufferLens[i])) { + buffer = backend.bufferViews[i].buffer; + c = 0; + while (startSample < endSample) { + callback(getByte(buffer, pos, byteRate), c); + if ((pos += byteRate) >= bufferLen) { + startSample++; + break; + } + else if (++c == data.channels) { + c = 0; + startSample++; } } } - else { - while (pos < __bufferLastSize) { - callback(getByte(__buffer, pos, __wordSize), c); - if (++c > buffer.channels) c = 0; - pos += __wordSize; - if ((n -= __wordSize) <= 0) break; - } - doRead = true; - ranOut = pos >= __bufferSize; - } + } + i++; + } - if (n <= 0) break; - else if (doRead && ranOut) { - __bufferLastSize = pos = 0; - __bufferTime = __bufferLastTime; + backend.mutex.release(); + + return startSample; + } + + // TODO: Fix this and _readDecoder in the future. + inline function _prepareDecoder():Bool { + return false; + /* + if (_decoder != null) return true; + if (data.decoder != null && (_decoder = data.decoder.clone()) != null) { + _bufferLen = (data.sampleRate >> 2) * _sampleSize; + #if cpp + if (_buffer != null) { + if (_buffer.length < _bufferLen) { + _buffer.getData().resize(_bufferLen); + _buffer.fill(_buffer.length, _bufferLen - _buffer.length, 0); + @:privateAccess _buffer.length = _bufferLen; } } + else + #end + _buffer = new ArrayBuffer(_bufferLen); + return true; + } + return false; + */ + } + + /* + function _readDecoder(startSample:Int, endSample:Int, callback:ReadCallback) { + var pos = (startSample - _bufferLastSample) * _sampleSize, n = endSample - startSample, c = 0; + + var doDecode = _bufferLastSize == 0 || (pos < 0 && pos >= _bufferLastSize); + if (doDecode) { + _decoder.seek(startSample); + _bufferLastSize = pos = 0; + doDecode = true; + } + + var result:Int; + while (n > 0) { + if (doDecode) { + _bufferLastSample = _decoder.tell(); + result = _decoder.decode(_buffer, pos, _bufferLen - pos); + if (result == 0) break; + + _bufferLastSize += result; + while (n > 0) { + callback(getByte(_buffer, pos, data.byteRate), c); + if (++c == data.channels) { + c = 0; + n--; + } + if ((pos += data.byteRate) >= _bufferLastSize) break; + } + } + else { + while (n > 0) { + callback(getByte(_buffer, pos, data.byteRate), c); + if (++c == data.channels) { + c = 0; + n--; + } + if ((pos += data.byteRate) >= _bufferLastSize) break; + } + doDecode = true; + _bufferLastSize = pos = 0; + } } } - #end - #end -} \ No newline at end of file + */ + + static inline function idiv(num:Int, denom:Int):Int return #if (cpp && !cppia) cpp.NativeMath.idiv(num, denom) #else Std.int(num / denom) #end; +} +#end \ No newline at end of file diff --git a/source/funkin/backend/utils/NativeAPI.hx b/source/funkin/backend/utils/NativeAPI.hx index d3b5e758..16e08c19 100644 --- a/source/funkin/backend/utils/NativeAPI.hx +++ b/source/funkin/backend/utils/NativeAPI.hx @@ -13,12 +13,6 @@ import flixel.util.FlxColor; * Some functions might not have effect on some platforms. */ class NativeAPI { - @:dox(hide) public static function registerAudio() { - #if windows - Windows.registerAudio(); - #end - } - @:dox(hide) public static function registerAsDPICompatible() { #if windows Windows.registerAsDPICompatible(); @@ -185,16 +179,7 @@ class NativeAPI { public static function setConsoleColors(foregroundColor:ConsoleColor = NONE, ?backgroundColor:ConsoleColor = NONE) { if(Main.noTerminalColor) return; - #if (windows && !hl) - if(foregroundColor == NONE) - foregroundColor = LIGHTGRAY; - if(backgroundColor == NONE) - backgroundColor = BLACK; - - var fg:Int = cast foregroundColor; - var bg:Int = cast backgroundColor; - Windows.setConsoleColors((bg * 16) + fg); - #elseif sys + #if sys Sys.print("\x1b[0m"); if(foregroundColor != NONE) Sys.print("\x1b[" + Std.int(consoleColorToANSI(foregroundColor)) + "m"); diff --git a/source/funkin/backend/utils/native/Windows.hx b/source/funkin/backend/utils/native/Windows.hx index 545e34b6..1ac32c5b 100644 --- a/source/funkin/backend/utils/native/Windows.hx +++ b/source/funkin/backend/utils/native/Windows.hx @@ -27,107 +27,10 @@ import funkin.backend.utils.NativeAPI.MessageBoxIcon; #include #include #include - -#define SAFE_RELEASE(punk) \\ - if ((punk) != NULL) \\ - { (punk)->Release(); (punk) = NULL; } - -static long lastDefId = 0; - -class AudioFixClient : public IMMNotificationClient { - LONG _cRef; - IMMDeviceEnumerator *_pEnumerator; - - public: - AudioFixClient() : - _cRef(1), - _pEnumerator(NULL) - { - HRESULT result = CoCreateInstance(__uuidof(MMDeviceEnumerator), - NULL, CLSCTX_INPROC_SERVER, - __uuidof(IMMDeviceEnumerator), - (void**)&_pEnumerator); - if (result == S_OK) { - _pEnumerator->RegisterEndpointNotificationCallback(this); - } - } - - ~AudioFixClient() - { - SAFE_RELEASE(_pEnumerator); - } - - ULONG STDMETHODCALLTYPE AddRef() - { - return InterlockedIncrement(&_cRef); - } - - ULONG STDMETHODCALLTYPE Release() - { - ULONG ulRef = InterlockedDecrement(&_cRef); - if (0 == ulRef) - { - delete this; - } - return ulRef; - } - - HRESULT STDMETHODCALLTYPE QueryInterface( - REFIID riid, VOID **ppvInterface) - { - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) - { - return S_OK; - }; - - HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) - { - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( - LPCWSTR pwstrDeviceId, - DWORD dwNewState) - { - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( - LPCWSTR pwstrDeviceId, - const PROPERTYKEY key) - { - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged( - EDataFlow flow, ERole role, - LPCWSTR pwstrDeviceId) - { - ::funkin::backend::_hx_system::Main_obj::audioDisconnected = true; - return S_OK; - }; -}; - -AudioFixClient *curAudioFix; ') @:dox(hide) final class Windows { - public static var __audioChangeCallback:Void->Void = function() { - trace("test"); - }; - - - @:functionCode(' - if (!curAudioFix) curAudioFix = new AudioFixClient(); - ') - public static function registerAudio() { - funkin.backend.system.Main.audioDisconnected = false; - } - @:functionCode(' int darkMode = enable ? 1 : 0; diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index cb4570d2..037e0a6c 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -1082,7 +1082,7 @@ class PlayState extends MusicBeatState if (notNull) PlayState.instance.gameAndCharsCall("onStageDestroy", [stage]); scripts.call("destroy"); - for (g in __cachedGraphics) g.useCount--; + for (g in __cachedGraphics) g.decrementUseCount(); @:privateAccess { for (strumLine in strumLines.members) FlxG.sound.destroySound(strumLine.vocals); if (FlxG.sound.music != inst) FlxG.sound.destroySound(inst); diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index e36e0e9c..2a67ddcf 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -228,6 +228,7 @@ class Options applyKeybinds(); applyQuality(); + flixel.sound.FlxSoundData.allowStreaming = streamedMusic; FlxG.sound.defaultMusicGroup.volume = volumeMusic; FlxG.autoPause = autoPause; if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; diff --git a/source/openfl/utils/Assets.hx b/source/openfl/utils/Assets.hx index c14b9f2c..e49ce293 100644 --- a/source/openfl/utils/Assets.hx +++ b/source/openfl/utils/Assets.hx @@ -3,7 +3,6 @@ package openfl.utils; #if !macro import funkin.backend.system.Main; import funkin.options.Options; -import funkin.backend.system.OptimizedBitmapData; #end import openfl.utils._internal.Log; import openfl.display.BitmapData; @@ -15,11 +14,11 @@ import openfl.media.Sound; import openfl.text.Font; #if lime import lime.app.Promise; +import lime.media.AudioBuffer; import lime.utils.AssetLibrary as LimeAssetLibrary; import lime.utils.Assets as LimeAssets; #end #if lime_vorbis -import lime.media.AudioBuffer; import lime.media.vorbis.VorbisFile; #end @@ -38,6 +37,11 @@ import lime.media.vorbis.VorbisFile; preloader by extending the `NMEPreloader` class, and specifying a custom preloader using in the project file. + + @see [Working with bitmap assets](https://books.openfl.org/openfl-developers-guide/working-with-bitmaps/working-with-bitmap-assets.html) + @see [Working with byte array assets](https://books.openfl.org/openfl-developers-guide/working-with-byte-arrays/working-with-byte-array-assets.html) + @see [Working with font assets](https://books.openfl.org/openfl-developers-guide/using-the-textfield-class/working-with-font-assets.html) + @see [Working with sound assets](https://books.openfl.org/openfl-developers-guide/working-with-sound/working-with-sound-assets.html) **/ #if !openfl_debug @:fileXml('tags="haxe,release"') @@ -49,6 +53,8 @@ import lime.media.vorbis.VorbisFile; @:access(openfl.utils.AssetLibrary) class Assets { + public static var allowCompressedTextures:Bool = false; + public static var cache:IAssetCache = new AssetCache(); @:noCompletion private static var dispatcher:EventDispatcher #if !macro = new EventDispatcher() #end; @@ -75,11 +81,29 @@ class Assets Returns whether a specific asset exists @param id The ID or asset path for the asset @param type The asset type to match, or null to match any type + @param allowCompressedTextures Whether to check for compressed texture formats (e.g., ASTC) when the asset is a PNG. Defaults to true. @return Whether the requested asset ID and type exists **/ - public static function exists(id:String, type:AssetType = null):Bool + public static function exists(id:String, type:AssetType = null, allowCompressedTextures:Bool = true):Bool { #if lime + #if !flash + if (allowCompressedTextures) + { + if (id != null && haxe.io.Path.extension(id) == "png") + { + if (LimeAssets.exists(haxe.io.Path.withExtension(id, "astc"), BINARY)) + { + return true; + } + } + else if (id != null && haxe.io.Path.extension(id) == "astc" && type != AssetType.BINARY) + { + type = AssetType.BINARY; + } + } + #end + return LimeAssets.exists(id, cast type); #else return false; @@ -87,26 +111,74 @@ class Assets } /** - Gets an instance of an embedded bitmap - @usage var bitmap = new Bitmap (Assets.getBitmapData ("image.png")); + Gets an instance of an embedded bitmap. + + ```haxe + var bitmap = new Bitmap (Assets.getBitmapData ("image.png")); + ``` + + _Note:_ This method may behave differently, depending on the target + platform. On targets that can quickly create new BitmapData instances + synchronously, every call to `Assets.getBitmapData()` with the same ID + will return a BitmapData instance with its own separate copy of the + underlying image data. However, on other targets where loading + BitmapData synchronously is unacceptably slow, or where BitmapData may + not be loaded synchronously at all (meaning that there is no choice but + to load it asynchronously), every call to `Assets.getBitmapData()` with + the same ID may return a BitmapData instance that shares the same + underlying image data each time. + + With that in mind, modifying or disposing the contents of the BitmapData + returned by `Assets.getBitmapData()` may affect the results of future + calls to `Assets.getBitmapData()` on some targets. To access a + BitmapData instance that may be modified or disposed without affecting + future calls to `Assets.getBitmapData()`, call the BitmapData instance's + `clone()` method to manually create a copy. + @param id The ID or asset path for the bitmap @param useCache (Optional) Whether to allow use of the asset cache (Default: true) - @param pushToGPU Whenever the image should be immediately pushed to GPU. + @param allowCompressedTextures (Optional) Wether to allow compressed textures to be used to get this bitmap (Default: true) @return A new BitmapData object + + @see [Working with bitmap assets](https://books.openfl.org/openfl-developers-guide/working-with-bitmaps/working-with-bitmap-assets.html) **/ - public static function getBitmapData(id:String, useCache:Bool = true, pushToGPU:Bool = true):BitmapData + public static function getBitmapData(id:String, useCache:Bool = true, allowCompressedTextures:Bool = true):BitmapData { #if (lime && tools && !display) if (useCache && cache.enabled && cache.hasBitmapData(id)) { var bitmapData = cache.getBitmapData(id); - if (isValidBitmapData(bitmapData) && (pushToGPU || bitmapData.readable)) + if (isValidBitmapData(bitmapData)) { return bitmapData; } } + #if !flash + if ((allowCompressedTextures || haxe.io.Path.extension(id) == "astc") && openfl.Lib.current.stage.context3D.isASTCSupported()) + { + final astcTexture:String = haxe.io.Path.withExtension(id, "astc"); + + if (LimeAssets.exists(astcTexture, BINARY)) + { + var bitmapData = BitmapData.fromTexture(openfl.Lib.current.stage.context3D.createASTCTexture(LimeAssets.getBytes(astcTexture)), false); + + if (useCache && cache.enabled) + { + cache.setBitmapData(id, bitmapData); + } + + return bitmapData; + } + + if (haxe.io.Path.extension(id) == "astc") + { + return null; + } + } + #end + var image = LimeAssets.getImage(id, false); if (image != null) @@ -114,13 +186,11 @@ class Assets #if flash var bitmapData = image.src; #else - var bitmapData:BitmapData = null; - #if !macro if (pushToGPU && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) { - bitmapData = new OptimizedBitmapData(0, 0, true, 0); - bitmapData.__fromImage(image); - } else #end { - bitmapData = BitmapData.fromImage(image); - } + var bitmapData = BitmapData.fromImage(image); + if (Assets.allowCompressedTextures && allowCompressedTextures && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) + BitmapDataUtil.toHardware(bitmapData); + + bitmapData.__asset = true; #end if (useCache && cache.enabled) @@ -137,9 +207,15 @@ class Assets /** Gets an instance of an embedded binary asset - @usage var bytes = Assets.getBytes ("file.zip"); + + ```haxe + var bytes = Assets.getBytes ("file.zip"); + ``` + @param id The ID or asset path for the asset @return A new ByteArray object + + @see [Working with byte array assets](https://books.openfl.org/openfl-developers-guide/working-with-byte-arrays/working-with-byte-array-assets.html) **/ public static function getBytes(id:String):ByteArray { @@ -152,10 +228,16 @@ class Assets /** Gets an instance of an embedded font - @usage var fontName = Assets.getFont ("font.ttf").fontName; + + ```haxe + var fontName = Assets.getFont ("font.ttf").fontName; + ``` + @param id The ID or asset path for the font @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return A new Font object + + @see [Working with font assets](https://books.openfl.org/openfl-developers-guide/using-the-textfield-class/working-with-font-assets.html) **/ public static function getFont(id:String, useCache:Bool = true):Font { @@ -199,7 +281,11 @@ class Assets /** Gets an instance of an included MovieClip - @usage var movieClip = Assets.getMovieClip ("library:BouncingBall"); + + ```haxe + var movieClip = Assets.getMovieClip ("library:BouncingBall"); + ``` + @param id The ID for the MovieClip @return A new MovieClip object **/ @@ -241,26 +327,31 @@ class Assets return null; } - public static function getMusic(id:String, useCache:Bool = true, staticFallback:Bool = true):Sound + public static function getMusic(id:String, useCache:Bool = true):Sound { - if (useCache && staticFallback && cache.enabled && cache.hasSound(id)) { - var sound = cache.getSound(id); - if (isValidSound(sound)) return sound; - } - #if (lime_vorbis && lime > "7.9.0" && !macro) if (Options.streamedMusic) { + #if (lime_funkin && lime_native) + var path = getPath(id); + var buffer = AudioBuffer.fromFile(path, true); + if (buffer != null) return Sound.fromAudioBuffer(buffer); + #elseif (lime_vorbis && lime > "7.9.0") var path = getPath(id); - // TODO: What if it is a WAV or non-Vorbis file? var vorbisFile = VorbisFile.fromFile(path); - if (vorbisFile != null) return Sound.fromAudioBuffer(AudioBuffer.fromVorbisFile(vorbisFile)); + var buffer = AudioBuffer.fromVorbisFile(vorbisFile); + if (buffer != null) return Sound.fromAudioBuffer(buffer); + #end } - #end - return if (staticFallback) getSound(id, useCache); else null; + + return getSound(id, useCache); } /** Gets the file path (if available) for an asset - @usage var path = Assets.getPath ("file.txt"); + + ```haxe + var path = Assets.getPath ("file.txt"); + ``` + @param id The ID or asset path for the asset @return The path to the asset, or null if it does not exist **/ @@ -275,10 +366,16 @@ class Assets /** Gets an instance of an embedded sound - @usage var sound = Assets.getSound ("sound.wav"); + + ```haxe + var sound = Assets.getSound ("sound.wav"); + ``` + @param id The ID or asset path for the sound @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return A new Sound object + + @see [Working with sound assets](https://books.openfl.org/openfl-developers-guide/working-with-sound/working-with-sound-assets.html) **/ public static function getSound(id:String, useCache:Bool = true):Sound { @@ -317,7 +414,11 @@ class Assets /** Gets an instance of an embedded text asset - @usage var text = Assets.getText ("text.txt"); + + ```haxe + var text = Assets.getText ("text.txt"); + ``` + @param id The ID or asset path for the asset @return A new String object **/ @@ -438,7 +539,8 @@ class Assets return false; } #else - return (bitmapData != null && #if !lime_hybrid bitmapData.image != null #else bitmapData.__handle != null #end); + return (bitmapData != null + && #if !lime_hybrid (bitmapData.image != null || bitmapData.__texture != null) #else bitmapData.__handle != null #end); #end #else return true; @@ -471,12 +573,18 @@ class Assets /** Loads an included bitmap asset asynchronously - @usage Assets.loadBitmapData ("image.png").onComplete (handleImage); + + ```haxe + Assets.loadBitmapData ("image.png").onComplete (handleImage); + ``` + @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future + + @see [Working with bitmap assets](https://books.openfl.org/openfl-developers-guide/working-with-bitmaps/working-with-bitmap-assets.html) **/ - public static function loadBitmapData(id:String, useCache:Null = true):Future + public static function loadBitmapData(id:String, useCache:Null = true, allowCompressedTextures:Bool = true):Future { if (useCache == null) useCache = true; @@ -494,6 +602,47 @@ class Assets } } + #if !flash + if ((allowCompressedTextures || haxe.io.Path.extension(id) == "astc") && openfl.Lib.current.stage.context3D.isASTCSupported()) + { + final astcTexture:String = haxe.io.Path.withExtension(id, "astc"); + + if (LimeAssets.exists(astcTexture, BINARY)) + { + LimeAssets.loadBytes(astcTexture).onComplete(function(bytes) + { + if (bytes != null) + { + var bitmapData = BitmapData.fromTexture(openfl.Lib.current.stage.context3D.createASTCTexture(bytes), false); + if (Assets.allowCompressedTextures && allowCompressedTextures && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) + BitmapDataUtil.toHardware(bitmapData); + + bitmapData.__asset = true; + + if (useCache && cache.enabled) + { + cache.setBitmapData(id, bitmapData); + } + + promise.complete(bitmapData); + } + else + { + promise.error("[Assets] Could not load Image \"" + id + "\""); + } + }).onError(promise.error).onProgress(promise.progress); + + return promise.future; + } + + if (haxe.io.Path.extension(id) == "astc") + { + promise.error("[Assets] Could not load Image \"" + id + "\""); + return promise.future; + } + } + #end + LimeAssets.loadImage(id, false).onComplete(function(image) { if (image != null) @@ -501,13 +650,11 @@ class Assets #if flash var bitmapData = image.src; #else - var bitmapData:BitmapData = null; - #if !macro if (!Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) { - bitmapData = new OptimizedBitmapData(0, 0, true, 0); - bitmapData.__fromImage(image); - } else #end { - bitmapData = BitmapData.fromImage(image); - } + var bitmapData = BitmapData.fromImage(image); + if (Assets.allowCompressedTextures && allowCompressedTextures && !Main.forceGPUOnlyBitmapsOff && Options.gpuOnlyBitmaps) + BitmapDataUtil.toHardware(bitmapData); + + bitmapData.__asset = true; #end if (useCache && cache.enabled) @@ -531,9 +678,15 @@ class Assets /** Loads an included byte asset asynchronously - @usage Assets.loadBytes ("file.zip").onComplete (handleBytes); + + ```haxe + Assets.loadBytes ("file.zip").onComplete (handleBytes); + ``` + @param id The ID or asset path for the asset @return Returns a Future + + @see [Working with byte array assets](https://books.openfl.org/openfl-developers-guide/working-with-byte-arrays/working-with-byte-array-assets.html) **/ public static function loadBytes(id:String):Future { @@ -553,10 +706,16 @@ class Assets /** Loads an included font asset asynchronously - @usage Assets.loadFont ("font.ttf").onComplete (handleFont); + + ```haxe + Assets.loadFont ("font.ttf").onComplete (handleFont); + ``` + @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future + + @see [Working with font assets](https://books.openfl.org/openfl-developers-guide/using-the-textfield-class/working-with-font-assets.html) **/ public static function loadFont(id:String, useCache:Null = true):Future { @@ -617,8 +776,16 @@ class Assets } else { + // TODO: after Lime 8.2.0 is released, use conditional + // compilation to call LimeAssets.removeLibrary(name, false) + // since that is a new public API + @:privateAccess LimeAssets.libraries.remove(name); _library = new AssetLibrary(); _library.__proxy = library; + + // ERIC: Figure out what bug the change here was made to fix + // https://github.com/FunkinCrew/openfl/commit/7af97e4baff7371eba1f79959909ffca7971902e + // https://github.com/FunkinCrew/lime/commit/f195121ebec688b417e38ab115185c8d93c349d3 LimeAssets.registerLibrary(name, _library); } } @@ -632,7 +799,11 @@ class Assets /** Loads an included music asset asynchronously - @usage Assets.loadMusic ("music.ogg").onComplete (handleMusic); + + ```haxe + Assets.loadMusic ("music.ogg").onComplete (handleMusic); + ``` + @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future @@ -682,7 +853,11 @@ class Assets /** Loads an included MovieClip asset asynchronously - @usage Assets.loadMovieClip ("library:BouncingBall").onComplete (handleMovieClip); + + ```haxe + Assets.loadMovieClip ("library:BouncingBall").onComplete (handleMovieClip); + ``` + @param id The ID for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future @@ -724,10 +899,16 @@ class Assets /** Loads an included sound asset asynchronously - @usage Assets.loadSound ("sound.wav").onComplete (handleSound); + + ```haxe + Assets.loadSound ("sound.wav").onComplete (handleSound); + ``` + @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future + + @see [Working with sound assets](https://books.openfl.org/openfl-developers-guide/working-with-sound/working-with-sound-assets.html) **/ public static function loadSound(id:String, useCache:Null = true):Future { @@ -769,7 +950,11 @@ class Assets /** Loads an included text asset asynchronously - @usage Assets.loadText ("text.txt").onComplete (handleString); + + ```haxe + Assets.loadText ("text.txt").onComplete (handleString); + ``` + @param id The ID or asset path for the asset @param useCache (Optional) Whether to allow use of the asset cache (Default: true) @return Returns a Future