diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx index 34bd3277..48683859 100644 --- a/source/funkin/backend/utils/AudioAnalyzer.hx +++ b/source/funkin/backend/utils/AudioAnalyzer.hx @@ -2,11 +2,12 @@ package funkin.backend.utils; import flixel.sound.FlxSound; import lime.media.AudioBuffer; +import lime.utils.ArrayBufferView.ArrayBufferIO; +import lime.utils.ArrayBuffer; -#if lime_vorbis +#if (lime_cffi && lime_vorbis) import lime.media.vorbis.Vorbis; import lime.media.vorbis.VorbisFile; -import lime.utils.ArrayBuffer; #end /** @@ -17,193 +18,238 @@ import lime.utils.ArrayBuffer; * time, use that instead. **/ class AudioAnalyzer { + public static function getByte(buf:ArrayBuffer, pos:Int, wordSize:Int):Int { + if (wordSize == 2) return ArrayBufferIO.getInt16(buf, pos); + else if (wordSize == 3) { + var b = ArrayBufferIO.getUint16(buf, pos) | (buf.get(pos + 2) << 16); + if (b > 8388608) return b - 16777216; + else return b; + } + else if (wordSize == 4) return ArrayBufferIO.getInt32(buf, pos); + else return ArrayBufferIO.getUint8(buf, pos) - 128; + } + public var sound:FlxSound; public var buffer:AudioBuffer; + public var fftSize:Int; + + public var byteSize:Int; + var __toBits:Float; var __wordSize:Int; - var __bitUnsignedSize:Int; - var __bitSize:Int; + var __sampleSize:Int; + + #if (lime_cffi && lime_vorbis) + var __vorbis:VorbisFile; + var __buffer:ArrayBuffer; + var __bufferSize:Int; + #end + + // analyze var __min:Array = []; var __max:Array = []; - - //var __ln10:Float = 2.3025850929940456; - //var __ln2:Float = 0.6931471805599453; - #if lime_vorbis - var __vorbis:VorbisFile; - #end + var __minByte:Int; + var __maxByte:Int; /** * Creates an analyzer for specified FlxSound * @param sound An FlxSound to analyze. **/ - public function new(sound:FlxSound) { + public function new(sound:FlxSound, fftSize = 1024) { this.sound = sound; + this.fftSize = fftSize; __check(); } + function __check() if (sound.buffer != buffer) { + byteSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1); + + __toBits = buffer.sampleRate / 1000 * (__sampleSize = buffer.channels * (__wordSize = buffer.bitsPerSample >> 3)); + __min.resize(buffer.channels); + __max.resize(buffer.channels); + + #if (lime_cffi && lime_vorbis) __vorbis = null; #end + } + /** * TODO: IMPLEMENT FFT **/ public function getLevels(levels:Array, barCount:Int, duration:Float):Float { - __check(); return 0; } - inline function __check() if (sound.buffer != buffer) { - __bitSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1); - __bitUnsignedSize = 1 << buffer.bitsPerSample; - __toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3); - __vorbis = null; - __min.resize(buffer.channels); - __max.resize(buffer.channels); - } - /** * Returns a peak of an attached FlxSound from startPos to endPos in milliseconds. * @param startPos Start Position of the FlxSound in milliseconds. * @param endPos End Position of the FlxSound in milliseconds. - * @param outMin The output minimum value from the analyzer, indices is in channels (0 to -0.5 -> 0 to 0.5) (Optional) + * @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 Amplitude value **/ - public function analyze(startPos:Float, endPos:Float, ?outMin:Array, ?outMax:Array):Float { - __check(); - if (buffer.data != null) return __analyze(startPos, endPos, outMin, outMax); - #if lime_vorbis - else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outMin, outMax); - #end - return 0; + public function analyze(startPos:Float, endPos:Float, ?outOrOutMin:Array, ?outMax:Array):Float { + __minByte = __maxByte = 0; + + if (outOrOutMin != null) { + var f:Float; + if (outMax != null) { + for (i in 0...buffer.channels) __min[i] = __max[i] = 0; + + read(startPos, endPos, __analyzeCallback); + + 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 { + read(startPos, endPos, __analyzeCallbackSimple); + + outOrOutMin.resize(2); + if (outOrOutMin[0] < (f = __minByte / byteSize)) outOrOutMin[0] = f; + if (outOrOutMin[1] < (f = __maxByte / byteSize)) outOrOutMin[1] = f; + } + } + else + read(startPos, endPos, __analyzeCallbackSimple); + + return (__maxByte + __minByte) / byteSize; } - function __analyze(startPos:Float, endPos:Float, ?outMin:Array, ?outMax:Array):Float { - __prepareAmplitude(); + 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])))); - var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = buffer.data #if !js .buffer #end; - var c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize), b = 0, w = 0; - pos -= pos % __wordSize; - end -= end % __wordSize; + function __analyzeCallbackSimple(b:Int, c:Int):Void + ((b > __maxByte) ? (__maxByte = b) : (if (-b > __minByte) (__minByte = -b))); + + public function read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { + __check(); + if (buffer.data != null) __read(startPos, endPos, callback); + #if lime_cffi + else if (__canReadStream() && (startPos += __readStream(startPos, endPos, callback)) >= endPos) return; + #if lime_vorbis + else if (__prepareDecoder()) __readDecoder(startPos, endPos, callback); + #end + #end + } + + inline function __read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { + var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), c = 0; + pos -= pos % __sampleSize; + end -= end % __sampleSize; while (pos < end) { - if (__wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - __bitSize; - else { - while (w < buffer.bitsPerSample) { - b |= #if js buf[pos] #else buf.get(pos) #end << w; - w += 8; - pos++; - } - if (b > __bitSize) b -= __bitUnsignedSize; - } - if (__max[c] < b) __max[c] = b; else if (__min[c] < (b = -b)) __min[c] = b; + callback(getByte(buffer.data.buffer, pos, __wordSize), c); if (++c > buffer.channels) c = 0; - w = b = 0; + pos += __wordSize; } - - return __getAmplitude(outMin, outMax); } - inline function __prepareAmplitude() for (i in 0...buffer.channels) __min[i] = __max[i] = 0; - inline function __getAmplitude(?outMin:Array, ?outMax:Array):Float { - var min = 0, max = 0, useOutput = outMin != null && outMax != null, v:Float; - for (i in 0...buffer.channels) { - if (__min[i] > min) min = __min[i]; - if (__max[i] > max) max = __max[i]; - if (useOutput) { - if (outMin[i] < (v = __min[i] / __bitSize)) outMin[i] = v; - if (outMax[i] < (v = __max[i] / __bitSize)) outMax[i] = v; + #if lime_cffi + inline function __canReadStream():Bool + @:privateAccess return sound._source != null && sound._source.__backend != null && sound._source.__backend.streamTimer != null; + + inline function __readStream(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback):Float @:privateAccess { + var backend = sound._source.__backend; + var i = backend.bufferSizes.length - backend.queuedBuffers; + var time = backend.bufferTimes[i] * 1000; + + var n = Math.floor((endPos - startPos) * __toBits); + if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) { + var pos = Math.floor((startPos - time) * __toBits), buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i], c = 0; + pos -= pos % __sampleSize; + + while (n > 0) { + callback(getByte(buf, pos, __wordSize), c); + if (++c > buffer.channels) c = 0; + if ((pos += __wordSize) >= size) { + if (++i >= backend.bufferDatas.length) break; + buf = backend.bufferDatas[i].buffer; + size = backend.bufferSizes[i]; + pos = 0; + } + n -= __wordSize; } } - return (max + min) / __bitSize; + return endPos - (n / __toBits); } - #if lime_vorbis // As far i know, only native supports vorbis - var __buffer:ArrayBuffer; - var __bufferSize:Int; - function __prepareVorbis():Bool @:privateAccess { + #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 = 0x400 * buffer.channels * (buffer.bitsPerSample >> 3)); + __buffer = new ArrayBuffer(__bufferSize = 0x400 * __sampleSize); return true; } return false; } - function __analyzeVorbis(startPos:Float, endPos:Float, ?outMin:Array, ?outMax:Array):Float @:privateAccess { - var n = Math.floor((endPos - startPos) * __toBits); - if ((n -= n % __wordSize) < __wordSize) return 0; - - var pos = 0, c = 0, b = 0, w = 0; - __prepareAmplitude(); - - var backend = sound._source != null ? sound._source.__backend : null; - if (backend != null && backend.streamTimer != null) { - var i = backend.bufferSizes.length - backend.queuedBuffers; - var time = backend.bufferTimes[i] * 1000; - - if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) { - var buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i]; - c = Math.floor(((pos = Math.floor((startPos - time) * __toBits)) % (__wordSize * buffer.channels)) / __wordSize); - pos -= pos % __wordSize; - - while (n > 0) { - if (__wordSize == 1) b = buf.get(pos++) - __bitSize; - else { - while (w < buffer.bitsPerSample) { - b |= buf.get(pos) << w; - w += 8; - pos++; - } - if (b > __bitSize) b -= __bitUnsignedSize; - } - if (__max[c] < b) __max[c] = b; else if (__min[c] < (b = -b)) __min[c] = b; - if (++c > buffer.channels) c = 0; - if (pos >= size) { - if (++i >= backend.bufferDatas.length) break; - size = backend.bufferSizes[i]; - buf = backend.bufferDatas[i].buffer; - pos = 0; - } - w = b = 0; - n -= __wordSize; - } - - if (n == 0) return __getAmplitude(outMin, outMax); - startPos = pos / __toBits + time; - pos = c = 0; - } - } - - if (Math.abs(startPos - __vorbis.timeTell() * 1000) > 4) { + inline function __readDecoder(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) { + var time = startPos / 1000; + if (Math.abs(time - __vorbis.timeTell()) > 0.004) { if (startPos < 1) __vorbis.rawSeek(0); - else __vorbis.timeSeek(startPos / 1000); + else __vorbis.timeSeek(time); } var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, result; - while (n > 0) { - n -= (result = __vorbis.read(__buffer, 0, n < __bufferSize ? n : __bufferSize, isBigEndian, __wordSize, true)); + var n = Math.floor((endPos - startPos) * __toBits), pos = 0, c = 0; + n -= n % __sampleSize; + while (n > 0) { + result = __vorbis.read(__buffer, 0, n < __bufferSize ? n : __bufferSize, isBigEndian, __wordSize, true); if (result == Vorbis.HOLE) continue; else if (result <= 0) break; while (pos < result) { - if (__wordSize == 1) b = __buffer.get(pos++) - __bitSize; - else { - while (w < buffer.bitsPerSample) { - b |= __buffer.get(pos) << w; - w += 8; - pos++; - } - if (b > __bitSize) b -= __bitUnsignedSize; - } - if (__max[c] < b) __max[c] = b; else if (__min[c] < (b = -b)) __min[c] = b; + callback(getByte(__buffer, pos, __wordSize), c); if (++c > buffer.channels) c = 0; - w = b = 0; + if ((pos += __wordSize) >= n) break; } pos = 0; + n -= result; } - - return __getAmplitude(outMin, outMax); } #end -} \ No newline at end of file + #end +} + +private typedef AudioAnalyzerCallback = Int->Int->Void; + +/* +abstract Complex(Array) { + public var real(get, never):Float; + inline function get_real() return this[0]; + + public var imag(get, never):Float; + inline function get_imag() return this[1]; + + public inline function new(real:Float, imag:Float) this = [real, imag]; + public static inline function fromReal(real:Float):Complex return new Complex(real, 0); + public static inline function exp(w:Float):Complex return new Complex(Math.cos(w), Math.sin(w)); + + public var angle(get, never):Float; + inline function get_angle() return Math.atan2(imag, real); + + public var magnitude(get, never):Float; + inline function get_magnitude():Float return Math.sqrt(real*real + imag*imag); + + @:op(A + B) + public inline function add(rhs:Complex):Complex return new Complex(real + rhs.real, imag + rhs.imag); + + @:op(A - B) + public inline function sub(rhs:Complex):Complex return new Complex(real - rhs.real, imag - rhs.imag); + + @:op(A * B) + public inline function mult(rhs:Complex):Complex + return new Complex(real*rhs.real - imag*rhs.imag, real*rhs.imag + imag*rhs.real); + + @:op(A / B) + public inline function div(rhs:Complex):Complex { + var m = rhs.magnitude; + return new Complex((real*rhs.real + imag*rhs.imag) / m, (imag*rhs.real - real*rhs.imag) / m); + } + + public inline function conj():Complex return new Complex(real, -imag); +} +*/ \ No newline at end of file diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx index ae3ddf72..4d9323f5 100644 --- a/source/openfl/media/SoundChannel.hx +++ b/source/openfl/media/SoundChannel.hx @@ -27,7 +27,7 @@ import lime.media.openal.AL; @:fileXml('tags="haxe,release"') @:noDebug #end -#if (lime && lime_cffi) +#if lime_cffi @:access(lime._internal.backend.native.NativeAudioSource) @:access(lime.media.AudioSource) #end @@ -158,9 +158,9 @@ import lime.media.openal.AL; if (!__isValid) return false; var buffer = __source.buffer; - var wordSize = buffer.bitsPerSample >> 3, bitUnsignedSize = 1 << buffer.bitsPerSample, bitSize = 1 << (buffer.bitsPerSample - 1); - var pos = Math.floor(time * buffer.sampleRate / 1000 * buffer.channels * wordSize), size = 0, buf; - var leftMin = 0, leftMax = 0, rightMin = 0, rightMax = 0; + var wordSize = buffer.bitsPerSample >> 3, byteSize = 1 << (buffer.bitsPerSample - 1); + var pos = Math.floor(time * buffer.sampleRate / 1000 * buffer.channels * wordSize); + var leftMin = 0, leftMax = 0, rightMin = 0, rightMax = 0, size = 0, buf; #if lime_cffi var backend = __source.__backend, i = 0; @@ -175,25 +175,14 @@ import lime.media.openal.AL; size = #if js buf.byteLength #else buf.length #end; } - var s = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = Math.floor((pos % (wordSize * buffer.channels)) / wordSize), b = 0, w = 0; - pos -= pos % wordSize; + var s = Math.floor(Math.min(buffer.sampleRate / 80, 512)), c = 0, b; + pos -= pos % (buffer.channels * wordSize); while (s > 0) { - if (wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - bitSize; - else { - while (w < buffer.bitsPerSample) { - b |= #if js buf[pos] #else buf.get(pos) #end << w; - w += 8; - pos++; - } - if (b > bitSize) b -= bitUnsignedSize; - } - if (c % 2 == 0) { - if (leftMax < b) leftMax = b; else if (leftMin < (b = -b)) leftMin = b; - } - else if (rightMax < b) rightMax = b; else if (rightMin < (b = -b)) rightMin = b; - - if (pos >= size) #if lime_cffi { + b = funkin.backend.utils.AudioAnalyzer.getByte(buf, pos, wordSize); + if (c % 2 == 0) ((b > leftMax) ? (leftMax = b) : (if ((b = -b) > leftMin) (leftMin = b))); + else ((b > rightMax) ? (rightMax = b) : (if ((b = -b) > rightMin) (rightMin = b))); + if ((pos += wordSize) >= size) #if lime_cffi { if (!backend.streamed || ++i >= backend.bufferDatas.length) break; size = backend.bufferSizes[i]; buf = backend.bufferDatas[i].buffer; @@ -205,13 +194,12 @@ import lime.media.openal.AL; c = 0; s--; } - w = b = 0; } - if (buffer.channels == 1) __rightPeak = (__leftPeak = (leftMax + leftMin) / bitSize); + if (buffer.channels == 1) __rightPeak = (__leftPeak = (leftMax + leftMin) / byteSize); else { - __leftPeak = (leftMax + leftMin) / bitSize; - __rightPeak = (rightMax + rightMin) / bitSize; + __leftPeak = (leftMax + leftMin) / byteSize; + __rightPeak = (rightMax + rightMin) / byteSize; } #end