From 23432cb596c3265fd46cc19feb0560456d9ee512 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Sun, 27 Jul 2025 13:57:48 +0700
Subject: [PATCH 01/11] Remove funkin.vis
---
libs.xml | 2 --
1 file changed, 2 deletions(-)
diff --git a/libs.xml b/libs.xml
index 403f646d..6b16246e 100644
--- a/libs.xml
+++ b/libs.xml
@@ -21,8 +21,6 @@
-
-
From c6c7cdd32bc24c0e3300a17116253502bc559f49 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Sun, 27 Jul 2025 18:29:24 +0700
Subject: [PATCH 02/11] Fix & Change a bit of AudioAnalyzer base functions
---
source/funkin/backend/utils/AudioAnalyzer.hx | 104 +++++++++++--------
1 file changed, 58 insertions(+), 46 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index 0ad9af3d..90d47d38 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -18,7 +18,11 @@ class AudioAnalyzer {
var __wordSize:Int;
var __bitUnsignedSize:Int;
var __bitSize:Int;
+ var __min:Array = [];
+ var __max:Array = [];
+ //var __ln10:Float = 2.3025850929940456;
+ //var __ln2:Float = 0.6931471805599453;
#if lime_vorbis
var __vorbis:VorbisFile;
#end
@@ -28,36 +32,47 @@ class AudioAnalyzer {
__check();
}
+ public function getLevels(levels:Array, barCount:Int, duration:Float):Float {
+ __check();
+ // TODO: implement FFT
+ 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);
}
- public function analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float {
+ /**
+ * 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 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, outSeperate);
+ if (buffer.data != null) return __analyze(startPos, endPos, outMin, outMax);
#if lime_vorbis
- else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outSeperate);
+ else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outMin, outMax);
#end
return 0;
}
- inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array):Float {
- var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = buffer.data #if !js .buffer #end;
- var max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
- if (useSeperate) {
- while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
- while (c++ < buffer.channels) outSeperate.push(0);
- c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
- }
+ function __analyze(startPos:Float, endPos:Float, ?outMin:Array, ?outMax:Array):Float {
+ __prepareAmplitude();
+ 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;
while (pos < end) {
- // 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0)
if (__wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - __bitSize;
else {
while (w < buffer.bitsPerSample) {
@@ -67,15 +82,27 @@ class AudioAnalyzer {
}
if (b > __bitSize) b -= __bitUnsignedSize;
}
- if (max < b) max = b;
- if (useSeperate) {
- if (outSeperate[c] < b) outSeperate[c] = b;
- if (++c > buffer.channels) c = 0;
- }
+ if (__max[c] < b) __max[c] = b; else if (__min[c] < (b = -b)) __min[c] = b;
+ if (++c > buffer.channels) c = 0;
w = b = 0;
}
- return max / __bitSize;
+ 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;
+ }
+ }
+
+ return (max + min) / __bitSize;
}
#if lime_vorbis // As far i know, only native supports vorbis
@@ -91,17 +118,13 @@ class AudioAnalyzer {
return false;
}
- inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array):Float @:privateAccess {
+ 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, max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
- if (useSeperate) {
- while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
- while (c++ < buffer.channels) outSeperate.push(0);
- }
+ var pos = 0, c = 0, b = 0, w = 0;
+ __prepareAmplitude();
- // IT SHOULD BE ALWAYS BACKEND.STREAMED IF THIS GETS CALLED.
var backend = sound._source != null ? sound._source.__backend : null;
if (backend != null && backend.streamTimer != null) {
var i = backend.bufferSizes.length - backend.queuedBuffers;
@@ -109,29 +132,21 @@ class AudioAnalyzer {
if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) {
var buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i];
- pos = Math.floor((startPos - time) * __toBits);
- c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
+ 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;
- n--;
- }
+ if (__wordSize == 1) b = buf.get(pos++) - __bitSize;
else {
while (w < buffer.bitsPerSample) {
b |= buf.get(pos) << w;
w += 8;
pos++;
- n--;
}
if (b > __bitSize) b -= __bitUnsignedSize;
}
- if (max < b) max = b;
- if (useSeperate) {
- if (outSeperate[c] < b) outSeperate[c] = b;
- if (++c > buffer.channels) c = 0;
- }
+ 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];
@@ -139,20 +154,20 @@ class AudioAnalyzer {
pos = 0;
}
w = b = 0;
+ n -= __wordSize;
}
+ if (n == 0) return __getAmplitude(outMin, outMax);
startPos = pos / __toBits + time;
- // Cannot inline a not final return
+ pos = c = 0;
}
}
- if (n == 0) return max / __bitSize;
if (Math.abs(startPos - __vorbis.timeTell() * 1000) > 4) {
if (startPos < 1) __vorbis.rawSeek(0);
else __vorbis.timeSeek(startPos / 1000);
}
- pos = c = 0;
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));
@@ -170,17 +185,14 @@ class AudioAnalyzer {
}
if (b > __bitSize) b -= __bitUnsignedSize;
}
- if (max < b) max = b;
- if (useSeperate) {
- if (outSeperate[c] < b) outSeperate[c] = b;
- if (++c > buffer.channels) c = 0;
- }
+ if (__max[c] < b) __max[c] = b; else if (__min[c] < (b = -b)) __min[c] = b;
+ if (++c > buffer.channels) c = 0;
w = b = 0;
}
pos = 0;
}
- return max / __bitSize;
+ return __getAmplitude(outMin, outMax);
}
#end
}
\ No newline at end of file
From 92ee5a280ace81b7c272e45c6495ba692400b2fe Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Sun, 27 Jul 2025 18:49:23 +0700
Subject: [PATCH 03/11] fix SoundChannel peaks & give AudioAnalyzer desc
---
source/funkin/backend/utils/AudioAnalyzer.hx | 17 ++++++++++++++---
source/openfl/media/SoundChannel.hx | 14 ++++++++------
2 files changed, 22 insertions(+), 9 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index 90d47d38..34bd3277 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -9,8 +9,13 @@ import lime.media.vorbis.VorbisFile;
import lime.utils.ArrayBuffer;
#end
-// ORIGINAL CODES FROM YOSH & LUNAR https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx
-// REWRITTEN BY RALTYRO
+/**
+ * An utility that analyze FlxSounds,
+ * can be used to make waveform or real-time audio visualizer.
+ *
+ * FlxSound.amplitude does work in CNE so if any case if your only checking for peak of current
+ * time, use that instead.
+**/
class AudioAnalyzer {
public var sound:FlxSound;
public var buffer:AudioBuffer;
@@ -27,14 +32,20 @@ class AudioAnalyzer {
var __vorbis:VorbisFile;
#end
+ /**
+ * Creates an analyzer for specified FlxSound
+ * @param sound An FlxSound to analyze.
+ **/
public function new(sound:FlxSound) {
this.sound = sound;
__check();
}
+ /**
+ * TODO: IMPLEMENT FFT
+ **/
public function getLevels(levels:Array, barCount:Int, duration:Float):Float {
__check();
- // TODO: implement FFT
return 0;
}
diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx
index 6ccbf8ac..ae3ddf72 100644
--- a/source/openfl/media/SoundChannel.hx
+++ b/source/openfl/media/SoundChannel.hx
@@ -152,9 +152,7 @@ import lime.media.openal.AL;
@:noCompletion private function __updatePeaks(time:Float):Bool
{
if (Math.abs(time - __lastPeakTime) < 8) return false;
-
__lastPeakTime = time;
- __leftPeak = __rightPeak = 0;
#if !macro
if (!__isValid) return false;
@@ -162,6 +160,7 @@ import lime.media.openal.AL;
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;
#if lime_cffi
var backend = __source.__backend, i = 0;
@@ -189,7 +188,10 @@ import lime.media.openal.AL;
}
if (b > bitSize) b -= bitUnsignedSize;
}
- ((c % 2 == 0) ? (if (__leftPeak < b) __leftPeak = b) : (if (__rightPeak < b) __rightPeak = b));
+ 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 {
if (!backend.streamed || ++i >= backend.bufferDatas.length) break;
@@ -206,10 +208,10 @@ import lime.media.openal.AL;
w = b = 0;
}
- if (buffer.channels == 1) __rightPeak = __leftPeak / bitSize;
+ if (buffer.channels == 1) __rightPeak = (__leftPeak = (leftMax + leftMin) / bitSize);
else {
- __leftPeak = __leftPeak / bitSize;
- __rightPeak = __rightPeak / bitSize;
+ __leftPeak = (leftMax + leftMin) / bitSize;
+ __rightPeak = (rightMax + rightMin) / bitSize;
}
#end
From 16f5d5010f20ff99731fb9e750b1f23d1a6dea71 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Sun, 27 Jul 2025 18:50:35 +0700
Subject: [PATCH 04/11] Completely remove funkin.vis
---
project.xml | 5 -----
1 file changed, 5 deletions(-)
diff --git a/project.xml b/project.xml
index df54e556..97c07c70 100644
--- a/project.xml
+++ b/project.xml
@@ -153,9 +153,6 @@
-
-
-
@@ -196,8 +193,6 @@
-
-
From 2724c8b236cd39b720a33a80a0f93e7fe5237957 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Wed, 30 Jul 2025 20:48:42 +0700
Subject: [PATCH 05/11] Cleanup AudioAnalyzer
---
source/funkin/backend/utils/AudioAnalyzer.hx | 298 +++++++++++--------
source/openfl/media/SoundChannel.hx | 38 +--
2 files changed, 185 insertions(+), 151 deletions(-)
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
From 1f5e048945077ff073daf546d45f46085008c5aa Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Thu, 31 Jul 2025 19:32:21 +0700
Subject: [PATCH 06/11] First attempt on reimplementing FFT
---
source/funkin/backend/utils/AudioAnalyzer.hx | 267 ++++++++++++++-----
1 file changed, 193 insertions(+), 74 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index 48683859..b05ad285 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -10,6 +10,8 @@ import lime.media.vorbis.Vorbis;
import lime.media.vorbis.VorbisFile;
#end
+typedef AudioAnalyzerCallback = Int->Int->Void;
+
/**
* An utility that analyze FlxSounds,
* can be used to make waveform or real-time audio visualizer.
@@ -19,21 +21,21 @@ import lime.media.vorbis.VorbisFile;
**/
class AudioAnalyzer {
public static function getByte(buf:ArrayBuffer, pos:Int, wordSize:Int):Int {
- if (wordSize == 2) return ArrayBufferIO.getInt16(buf, pos);
+ if (wordSize == 2) return inline ArrayBufferIO.getInt16(buf, pos);
else if (wordSize == 3) {
- var b = ArrayBufferIO.getUint16(buf, pos) | (buf.get(pos + 2) << 16);
+ var b = inline 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;
+ else if (wordSize == 4) return inline ArrayBufferIO.getInt32(buf, pos);
+ else return inline ArrayBufferIO.getUint8(buf, pos) - 128;
}
public var sound:FlxSound;
- public var buffer:AudioBuffer;
- public var fftSize:Int;
+ public var buffer(default, null):AudioBuffer;
+ public var fftSamples(default, set):Int;
- public var byteSize:Int;
+ public var byteSize(default, null):Int;
var __toBits:Float;
var __wordSize:Int;
@@ -51,66 +53,178 @@ class AudioAnalyzer {
var __minByte:Int;
var __maxByte:Int;
+ // samples
+ var __sampleIndex:Int;
+ var __sampleOutputLength:Int;
+ var __sampleOutput:Array;
+
+ // fft
+ var __logN:Int;
+ var __freqSamples:Array;
+ var __reverseIndices:Array = [];
+ var __rootReals:Array = [];
+ var __rootImags:Array = [];
+ var __freqReals:Array = [];
+ var __freqImags:Array = [];
+
+ // levels
+ var __frequencies:Array;
+
/**
* Creates an analyzer for specified FlxSound
* @param sound An FlxSound to analyze.
**/
- public function new(sound:FlxSound, fftSize = 1024) {
+ public function new(sound:FlxSound, fftSamples = 512) {
this.sound = sound;
- this.fftSize = fftSize;
+ this.fftSamples = fftSamples;
__check();
}
function __check() if (sound.buffer != buffer) {
byteSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1);
+ #if (lime_cffi && lime_vorbis) __vorbis = null; #end
+
__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
+ inline function set_fftSamples(v:Int):Int {
+ if (fftSamples == (fftSamples = v)) return v;
+
+ var half = v >> 1, a, c;
+ __logN = Math.floor(Math.log(v) / Math.log(2));
+ __reverseIndices.resize(half);
+ __rootReals.resize(half);
+ __rootImags.resize(half);
+ __freqReals.resize(half);
+ __freqImags.resize(half);
+ for (i in 0...half) {
+ __reverseIndices[i] = __bitReverse(i);
+ __rootReals[i] = Math.cos(a = 2 * Math.PI * i / v);
+ __rootImags[i] = Math.sin(a);
+ }
+
+ return v;
+ }
+
+ inline function __bitReverse(x:Int):Int {
+ var y = 0, i = __logN;
+ while (i > 0) {
+ y = (y << 1) | (x & 1);
+ x >>= 1;
+ i--;
+ }
+ return y;
+ }
+
+ public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, minDb = -70.0, maxDb = -20.0):Array {
+ __frequencies = getFrequencies(startPos, __frequencies);
+
+ if (levels == null) levels = [];
+ levels.resize(barCount);
+
+ var x1 = 0.5, i1 = 1, n = fftSamples >> 1;
+ var x2, i2, v;
+ for (i in 0...barCount) {
+ i2 = Math.floor(x2 = Math.pow(n, (i + 1) / barCount) - 0.5);
+
+ if (i2 < i1) v = __frequencies[i2] * (x2 - x1);
+ else {
+ v = __frequencies[i1 - 1] * (i1 - x1);
+ while (i1 < i2) v += __frequencies[i1++];
+ if (i2 < n) v += __frequencies[i2] * (x2 - i2);
+ }
+
+ i1 = Math.ceil(x1 = x2);
+
+ v = 20 * Math.log(v * barCount / 12) / 2.302585092994046/*log(10)*/ / (maxDb - minDb);
+ if (delta > 0 && levels[i] > v) levels[i] -= (levels[i] - v) * delta;
+ else levels[i] = v;
+ }
+
+ return levels;
}
/**
- * TODO: IMPLEMENT FFT
+ * Gets frequencies from an attached FlxSound from startPos with samples
+ * @return Output of frequencies ranging from 1 to fftSamples/2.
**/
- public function getLevels(levels:Array, barCount:Int, duration:Float):Float {
- return 0;
+ public function getFrequencies(startPos:Float, ?frequencies:Array):Array {
+ __freqSamples = getSamples(startPos, fftSamples, true, __freqSamples);
+
+ var inv = fftSamples >> 1;
+ for (i in 0...inv) {
+ __freqReals[__reverseIndices[i]] = __freqSamples[i];
+ __freqImags[i] = 0;
+ }
+
+ var h = 1, g = 0, b = 0, r = 0;
+ var tr, ti, i1, i2;
+ while (inv > 0) {
+ h <<= 1;
+ while (g < fftSamples) {
+ while (b < h) {
+ i2 = (i1 = g + b) + h;
+ tr = __rootReals[r] * __freqReals[i2] - __rootImags[r] * __freqImags[i2];
+ ti = __rootReals[r] * __freqImags[i2] + __rootImags[r] * __freqReals[i2];
+
+ __rootReals[i2] = __freqReals[i1] - tr;
+ __rootImags[i2] = __freqImags[i1] - ti;
+ __freqReals[i1] += tr;
+ __freqImags[i1] += ti;
+
+ b++;
+ r += inv;
+ }
+ b = r = 0;
+ g += h;
+ }
+ g = 0;
+
+ inv >>= 1;
+ }
+
+ if (frequencies == null) frequencies = [];
+ frequencies.resize(inv = fftSamples >> 1);
+
+ var mag;
+ for (i in 0...inv) frequencies[i] = (mag = Math.sqrt(__freqReals[i] * __freqReals[i] + __freqImags[i] * __freqImags[i])) < 0.001 ? 0 : mag;
+
+ return frequencies;
}
/**
- * Returns a peak of an attached FlxSound from startPos to endPos in milliseconds.
+ * Analyzes an attached FlxSound from startPos to endPos in milliseconds to get the amplitudes.
* @param startPos Start Position of the FlxSound in milliseconds.
* @param endPos End Position of the FlxSound 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 Amplitude value
+ * @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;
- if (outOrOutMin != null) {
+ __check();
+ __read(startPos, endPos, hasTwoOut ? __analyzeCallback : __analyzeCallbackSimple);
+
+ if (hasOut) {
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;
- }
+ 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 {
- 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;
}
@@ -121,9 +235,54 @@ class AudioAnalyzer {
function __analyzeCallbackSimple(b:Int, c:Int):Void
((b > __maxByte) ? (__maxByte = b) : (if (-b > __minByte) (__minByte = -b)));
+ /**
+ * Gets samples from startPos with given length of samples.
+ * @param startPos Start Position of the FlxSound in milliseconds.
+ * @param length Length of Samples.
+ * @param mono Merge all of the byte channels of samples in one channel instead (Optional).
+ * @param Output that gets passed into this function (Optional).
+ * @return Output of
+ **/
+ public function getSamples(startPos:Float, length:Int, mono = true, ?output:Array):Array {
+ ((output == null) ? (__sampleOutput = output = []) : (__sampleOutput = output)).resize((__sampleOutputLength = length) * (mono ? 1 : buffer.channels));
+ __sampleIndex = 0;
+
+ __check();
+ __read(startPos, startPos + (length / __toBits), mono ? __getSamplesCallbackMerge : __getSamplesCallback);
+
+ __sampleOutput = null;
+ return output;
+ }
+
+ // TODO: check later if this formula is correct.
+ function __getSamplesCallbackMerge(b:Int, c:Int):Void if (__sampleIndex < __sampleOutputLength) {
+ if (c == 0) __sampleOutput[__sampleIndex] = b / buffer.channels / byteSize;
+ else if (c == buffer.channels) {
+ __sampleOutput[__sampleIndex] += b / buffer.channels / byteSize;
+ __sampleIndex++;
+ }
+ else
+ __sampleOutput[__sampleIndex] += b / buffer.channels / byteSize;
+ }
+
+ function __getSamplesCallback(b:Int, c:Int):Void if (__sampleIndex < __sampleOutputLength) {
+ __sampleOutput[__sampleIndex] = b / byteSize;
+ __sampleIndex++;
+ }
+
+ /**
+ * Read an attached FlxSound from startPos to endPos in milliseconds with a callback.
+ * @param startPos Start Position of the FlxSound in milliseconds.
+ * @param endPos End Position of the FlxSound in milliseconds.
+ * @param callback Int->Int->Void Byte->Channels->Void Callback to get the byte of a sample.
+ **/
public function read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) {
__check();
- if (buffer.data != null) __read(startPos, endPos, callback);
+ __read(startPos, endPos, 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) return;
#if lime_vorbis
@@ -132,7 +291,7 @@ class AudioAnalyzer {
#end
}
- inline function __read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) {
+ inline function __readData(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;
@@ -212,44 +371,4 @@ class AudioAnalyzer {
}
#end
#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
+}
\ No newline at end of file
From 888f4135e927343b890776741e3012709c5112d3 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Fri, 1 Aug 2025 11:58:13 +0700
Subject: [PATCH 07/11] Fix size out of bound
---
source/openfl/media/SoundChannel.hx | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/source/openfl/media/SoundChannel.hx b/source/openfl/media/SoundChannel.hx
index 4d9323f5..e63d5909 100644
--- a/source/openfl/media/SoundChannel.hx
+++ b/source/openfl/media/SoundChannel.hx
@@ -168,6 +168,12 @@ import lime.media.openal.AL;
size = backend.bufferSizes[i = backend.bufferSizes.length - backend.queuedBuffers];
buf = backend.bufferDatas[i].buffer;
pos -= Math.floor(backend.bufferTimes[i] * buffer.sampleRate * buffer.channels * wordSize);
+ while (pos > size) {
+ if (++i >= backend.bufferSizes.length) return false;
+ pos -= size;
+ buf = backend.bufferDatas[i].buffer;
+ size = backend.bufferSizes[i];
+ }
}
else
#end {
@@ -183,10 +189,10 @@ import lime.media.openal.AL;
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;
+ if (!backend.streamed || ++i >= backend.bufferSizes.length) break;
pos = 0;
+ buf = backend.bufferDatas[i].buffer;
+ size = backend.bufferSizes[i];
}
#else break; #end
From aabfb0f3cd8890e197978dd8f81535a6ad0b4c67 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Fri, 1 Aug 2025 11:58:21 +0700
Subject: [PATCH 08/11] it works?
---
source/funkin/backend/utils/AudioAnalyzer.hx | 253 +++++++++++++------
1 file changed, 181 insertions(+), 72 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index b05ad285..67206dd6 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -24,7 +24,7 @@ class AudioAnalyzer {
if (wordSize == 2) return inline ArrayBufferIO.getInt16(buf, pos);
else if (wordSize == 3) {
var b = inline ArrayBufferIO.getUint16(buf, pos) | (buf.get(pos + 2) << 16);
- if (b > 8388608) return b - 16777216;
+ if (b & 0x800000 != 0) return b - 0x1000000;
else return b;
}
else if (wordSize == 4) return inline ArrayBufferIO.getInt32(buf, pos);
@@ -33,7 +33,7 @@ class AudioAnalyzer {
public var sound:FlxSound;
public var buffer(default, null):AudioBuffer;
- public var fftSamples(default, set):Int;
+ public var fftN(default, set):Int;
public var byteSize(default, null):Int;
@@ -59,11 +59,14 @@ class AudioAnalyzer {
var __sampleOutput:Array;
// fft
+ var __N2:Int;
var __logN:Int;
var __freqSamples:Array;
var __reverseIndices:Array = [];
- var __rootReals:Array = [];
- var __rootImags:Array = [];
+ var __factors:Array = [];
+ var __windows:Array = [];
+ var __twiddleReals:Array = [];
+ var __twiddleImags:Array = [];
var __freqReals:Array = [];
var __freqImags:Array = [];
@@ -74,9 +77,9 @@ class AudioAnalyzer {
* Creates an analyzer for specified FlxSound
* @param sound An FlxSound to analyze.
**/
- public function new(sound:FlxSound, fftSamples = 512) {
+ public function new(sound:FlxSound, fftN = 2048) {
this.sound = sound;
- this.fftSamples = fftSamples;
+ this.fftN = fftN;
__check();
}
@@ -90,23 +93,50 @@ class AudioAnalyzer {
__max.resize(buffer.channels);
}
- inline function set_fftSamples(v:Int):Int {
- if (fftSamples == (fftSamples = v)) return v;
-
- var half = v >> 1, a, c;
- __logN = Math.floor(Math.log(v) / Math.log(2));
- __reverseIndices.resize(half);
- __rootReals.resize(half);
- __rootImags.resize(half);
- __freqReals.resize(half);
- __freqImags.resize(half);
- for (i in 0...half) {
+ inline function set_fftN(v:Int):Int {
+ if (fftN == (fftN = nextPow2(v))) return fftN;
+
+ __logN = Math.floor(Math.log(fftN) / Math.log(2));
+ __N2 = fftN >> 1;
+ __freqReals.resize(fftN);
+ __freqImags.resize(fftN);
+ __reverseIndices.resize(fftN);
+ __windows.resize(fftN);
+ __twiddleReals.resize(fftN);
+ __twiddleImags.resize(fftN);
+
+ var f, a;
+ for (i in 0...fftN) {
+ f = i / (fftN - 1);
+ __windows[i] = 0.42 - 0.5 * Math.cos(Math.PI * 2 * f) + 0.08 * Math.cos(4 * Math.PI * f);
__reverseIndices[i] = __bitReverse(i);
- __rootReals[i] = Math.cos(a = 2 * Math.PI * i / v);
- __rootImags[i] = Math.sin(a);
+ __twiddleReals[i] = Math.cos(a = 2 * Math.PI * i / fftN);
+ __twiddleImags[i] = Math.sin(a);
}
- return v;
+ __factors.resize(0);
+
+ var inv = fftN;
+ while (inv > 0) {
+ if (inv % 4 == 0) {
+ __factors.push(4);
+ inv >>= 2;
+ }
+ else if (inv % 2 == 0) {
+ __factors.push(2);
+ inv >>= 1;
+ }
+ else
+ break;
+ }
+
+ return fftN;
+ }
+
+ inline function nextPow2(x:Int):Int {
+ var p = 1;
+ while (p < x) p <<= 1;
+ return p;
}
inline function __bitReverse(x:Int):Int {
@@ -119,27 +149,39 @@ class AudioAnalyzer {
return y;
}
- public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, minDb = -70.0, maxDb = -20.0):Array {
+ public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, dbRange = 50.0, minFreq = 20.0, maxFreq = 20000.0):Array {
__frequencies = getFrequencies(startPos, __frequencies);
if (levels == null) levels = [];
levels.resize(barCount);
- var x1 = 0.5, i1 = 1, n = fftSamples >> 1;
- var x2, i2, v;
- for (i in 0...barCount) {
- i2 = Math.floor(x2 = Math.pow(n, (i + 1) / barCount) - 0.5);
+ var logMin = Math.log(minFreq), logMax = Math.log(maxFreq);
+ inline function calculateScale(i:Int)
+ return CoolUtil.bound(Math.exp(logMin + ((logMax - logMin) * i / barCount)) * fftN / buffer.sampleRate, 0, __N2 - 1);
- if (i2 < i1) v = __frequencies[i2] * (x2 - x1);
+ var s1 = calculateScale(0), s2;
+ var i1 = Math.floor(s1), i2;
+ var v, range;
+ for (i in 0...barCount) {
+ i2 = Math.ceil(s2 = calculateScale(i));
+
+ if ((range = s2 - s1) < 1) {
+ if (i2 == i1) v = __frequencies[i1] * range;
+ else v = (s1 + (s2 - s1) * (s1 - i1)) * range;
+ }
else {
- v = __frequencies[i1 - 1] * (i1 - x1);
- while (i1 < i2) v += __frequencies[i1++];
- if (i2 < n) v += __frequencies[i2] * (x2 - i2);
+ v = __frequencies[i1] * (Math.ceil(s1) - i1);
+ while (++i1 < i2) v += __frequencies[i1];
+ v += __frequencies[i2] * (s2 - Math.floor(s2));
}
- i1 = Math.ceil(x1 = x2);
+ i1 = Math.floor(s1 = s2);
+
+ if (v > 0) v = 20 * Math.log(v) / 2.302585092994046;//log(10)
+ else v = -200;
+
+ v = CoolUtil.bound(1 + v / dbRange, 0, 1);
- v = 20 * Math.log(v * barCount / 12) / 2.302585092994046/*log(10)*/ / (maxDb - minDb);
if (delta > 0 && levels[i] > v) levels[i] -= (levels[i] - v) * delta;
else levels[i] = v;
}
@@ -149,48 +191,106 @@ class AudioAnalyzer {
/**
* Gets frequencies from an attached FlxSound from startPos with samples
- * @return Output of frequencies ranging from 1 to fftSamples/2.
+ * @return Output of frequencies ranging from 1 to fftN/2.
**/
public function getFrequencies(startPos:Float, ?frequencies:Array):Array {
- __freqSamples = getSamples(startPos, fftSamples, true, __freqSamples);
-
- var inv = fftSamples >> 1;
- for (i in 0...inv) {
- __freqReals[__reverseIndices[i]] = __freqSamples[i];
- __freqImags[i] = 0;
- }
-
- var h = 1, g = 0, b = 0, r = 0;
- var tr, ti, i1, i2;
- while (inv > 0) {
- h <<= 1;
- while (g < fftSamples) {
- while (b < h) {
- i2 = (i1 = g + b) + h;
- tr = __rootReals[r] * __freqReals[i2] - __rootImags[r] * __freqImags[i2];
- ti = __rootReals[r] * __freqImags[i2] + __rootImags[r] * __freqReals[i2];
-
- __rootReals[i2] = __freqReals[i1] - tr;
- __rootImags[i2] = __freqImags[i1] - ti;
- __freqReals[i1] += tr;
- __freqImags[i1] += ti;
-
- b++;
- r += inv;
- }
- b = r = 0;
- g += h;
- }
- g = 0;
-
- inv >>= 1;
- }
+ __freqSamples = getSamples(startPos, fftN, true, __freqSamples);
if (frequencies == null) frequencies = [];
- frequencies.resize(inv = fftSamples >> 1);
-
- var mag;
- for (i in 0...inv) frequencies[i] = (mag = Math.sqrt(__freqReals[i] * __freqReals[i] + __freqImags[i] * __freqImags[i])) < 0.001 ? 0 : mag;
+ frequencies.resize(__N2);
+
+ if (fftN == 1) frequencies[0] = __freqSamples[0];
+ else {
+ for (i in 0...fftN) {
+ __freqReals[__reverseIndices[i]] = __freqSamples[i] * __windows[i];
+ __freqImags[i] = 0;
+ }
+
+ // https://github.com/FunkinCrew/grig.audio/commit/8567c4dad34cfeaf2ff23fe12c3796f5db80685e
+ inline function butterfly4PointOptimized(i0:Int, i1:Int, i2:Int, i3:Int, w1_idx:Int, w2_idx:Int, w3_idx:Int) {
+ // Load input values
+ var x0r = __freqReals[i0];
+ var x0i = __freqImags[i0];
+
+ // Apply twiddle factors to x1, x2, x3
+ // x1 = workingData[i1] * twiddle1
+ var x1r_raw = __freqReals[i1];
+ var x1i_raw = __freqImags[i1];
+ var tw1r = __twiddleReals[w1_idx];
+ var tw1i = __twiddleImags[w1_idx];
+ var x1r = x1r_raw * tw1r - x1i_raw * tw1i;
+ var x1i = x1r_raw * tw1i + x1i_raw * tw1r;
+
+ // x2 = workingData[i2] * twiddle2
+ var x2r_raw = __freqReals[i2];
+ var x2i_raw = __freqImags[i2];
+ var tw2r = __twiddleReals[w2_idx];
+ var tw2i = __twiddleImags[w2_idx];
+ var x2r = x2r_raw * tw2r - x2i_raw * tw2i;
+ var x2i = x2r_raw * tw2i + x2i_raw * tw2r;
+
+ // x3 = workingData[i3] * twiddle3
+ var x3r_raw = __freqReals[i3];
+ var x3i_raw = __freqImags[i3];
+ var tw3r = __twiddleReals[w3_idx];
+ var tw3i = __twiddleImags[w3_idx];
+ var x3r = x3r_raw * tw3r - x3i_raw * tw3i;
+ var x3i = x3r_raw * tw3i + x3i_raw * tw3r;
+
+ // Compute intermediate values for 4-point DFT
+ var t0r = x0r + x2r; // (x0 + x2).real
+ var t0i = x0i + x2i; // (x0 + x2).imag
+ var t1r = x0r - x2r; // (x0 - x2).real
+ var t1i = x0i - x2i; // (x0 - x2).imag
+ var t2r = x1r + x3r; // (x1 + x3).real
+ var t2i = x1i + x3i; // (x1 + x3).imag
+ var t3r = x1r - x3r; // (x1 - x3).real
+ var t3i = x1i - x3i; // (x1 - x3).imag
+
+ // Apply j multiplication: j * (a + jb) = -b + ja
+ var jt3r = -t3i; // j * t3.real = -t3.imag
+ var jt3i = t3r; // j * t3.imag = t3.real
+
+ // Final 4-point DFT butterfly outputs
+ __freqReals[i0] = t0r + t2r; // X[k]
+ __freqImags[i0] = t0i + t2i;
+ __freqReals[i1] = t1r - jt3r; // X[k + N/4]
+ __freqImags[i1] = t1i - jt3i;
+ __freqReals[i2] = t0r - t2r; // X[k + N/2]
+ __freqImags[i2] = t0i - t2i;
+ __freqReals[i3] = t1r + jt3r; // X[k + 3N/4]
+ __freqImags[i3] = t1i + jt3i;
+ }
+
+ inline function butterfly2PointOptimized(i0:Int, i1:Int, w_idx:Int) {
+ var tempr = __freqReals[i1] * __twiddleReals[w_idx] - __freqImags[i1] * __twiddleImags[w_idx];
+ var tempi = __freqReals[i1] * __twiddleImags[w_idx] + __freqImags[i1] * __twiddleReals[w_idx];
+ __freqReals[i1] = __freqReals[i0] - tempr;
+ __freqImags[i1] = __freqImags[i0] - tempi;
+ __freqReals[i0] += tempr;
+ __freqImags[i0] += tempi;
+ }
+
+ var size = 1, n, s, start, t;
+ for (radix in __factors) {
+ n = Math.floor(fftN / (size *= radix));
+ s = size >> (radix >> 1);
+ if (radix == 4) for (i in 0...n) {
+ start = i * size;
+ for (k in 0...s)
+ butterfly4PointOptimized(t = start + k, t = (t + s), t = (t + s), t = (t + s),
+ (k * n) % fftN, (2 * k * n) % fftN, (3 * k * n) % fftN);
+ }
+ else for (i in 0...n) {
+ start = i * size;
+ for (k in 0...s) butterfly2PointOptimized(t = start + k, t = (t + s), (k * n) % fftN);
+ }
+ }
+
+ var inv = 1.0 / fftN;
+ frequencies[0] = Math.sqrt(__freqReals[0] * __freqReals[0] + __freqImags[0] * __freqImags[0]) * inv;
+ for (i in 1...__N2) frequencies[i] = 2 * Math.sqrt(__freqReals[i] * __freqReals[i] + __freqImags[i] * __freqImags[i]) * inv;
+ }
return frequencies;
}
@@ -248,7 +348,7 @@ class AudioAnalyzer {
__sampleIndex = 0;
__check();
- __read(startPos, startPos + (length / __toBits), mono ? __getSamplesCallbackMerge : __getSamplesCallback);
+ __read(startPos, startPos + (length / __toBits * buffer.channels), mono ? __getSamplesCallbackMerge : __getSamplesCallback);
__sampleOutput = null;
return output;
@@ -315,16 +415,25 @@ class AudioAnalyzer {
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;
+ while (pos > size) {
+ if (++i >= backend.bufferSizes.length) {
+ n = 0;
+ break;
+ }
+ pos -= size;
+ buf = backend.bufferDatas[i].buffer;
+ size = backend.bufferSizes[i];
+ }
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;
+ if (++i >= backend.bufferSizes.length) break;
+ pos = 0;
buf = backend.bufferDatas[i].buffer;
size = backend.bufferSizes[i];
- pos = 0;
}
n -= __wordSize;
}
From a9495eed5e7da7e756dca3571024a97743fd7647 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Fri, 1 Aug 2025 15:15:03 +0700
Subject: [PATCH 09/11] Uhhuhhh
---
source/funkin/backend/utils/AudioAnalyzer.hx | 201 +++++++++----------
1 file changed, 98 insertions(+), 103 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index 67206dd6..2eeedb6b 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -77,7 +77,7 @@ class AudioAnalyzer {
* Creates an analyzer for specified FlxSound
* @param sound An FlxSound to analyze.
**/
- public function new(sound:FlxSound, fftN = 2048) {
+ public function new(sound:FlxSound, fftN = 512) {
this.sound = sound;
this.fftN = fftN;
__check();
@@ -108,26 +108,23 @@ class AudioAnalyzer {
var f, a;
for (i in 0...fftN) {
f = i / (fftN - 1);
- __windows[i] = 0.42 - 0.5 * Math.cos(Math.PI * 2 * f) + 0.08 * Math.cos(4 * Math.PI * f);
+ __windows[i] = 0.42 - 0.5 * Math.cos(2 * Math.PI * f) + 0.08 * Math.cos(4 * Math.PI * f);
__reverseIndices[i] = __bitReverse(i);
- __twiddleReals[i] = Math.cos(a = 2 * Math.PI * i / fftN);
+ __twiddleReals[i] = Math.cos(a = -2 * Math.PI * i / fftN);
__twiddleImags[i] = Math.sin(a);
}
__factors.resize(0);
var inv = fftN;
- while (inv > 0) {
- if (inv % 4 == 0) {
- __factors.push(4);
- inv >>= 2;
- }
- else if (inv % 2 == 0) {
- __factors.push(2);
- inv >>= 1;
- }
- else
- break;
+ while (inv % 4 == 0) {
+ __factors.push(4);
+ inv >>= 2;
+ }
+
+ while (inv % 2 == 0) {
+ __factors.push(2);
+ inv >>= 1;
}
return fftN;
@@ -149,40 +146,37 @@ class AudioAnalyzer {
return y;
}
- public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, dbRange = 50.0, minFreq = 20.0, maxFreq = 20000.0):Array {
+ public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, minDb = -70, maxDb = -10, minFreq = 20.0, maxFreq = 22000.0):Array {
__frequencies = getFrequencies(startPos, __frequencies);
if (levels == null) levels = [];
levels.resize(barCount);
var logMin = Math.log(minFreq), logMax = Math.log(maxFreq);
+ var logRange = logMax - logMin, dbRange = maxDb - minDb;
inline function calculateScale(i:Int)
- return CoolUtil.bound(Math.exp(logMin + ((logMax - logMin) * i / barCount)) * fftN / buffer.sampleRate, 0, __N2 - 1);
+ return CoolUtil.bound(Math.exp(logMin + (logRange * i / (barCount + 1))) * fftN / buffer.sampleRate, 0, __N2 - 1);
var s1 = calculateScale(0), s2;
var i1 = Math.floor(s1), i2;
var v, range;
for (i in 0...barCount) {
- i2 = Math.ceil(s2 = calculateScale(i));
-
- if ((range = s2 - s1) < 1) {
+ if ((range = (s2 = calculateScale(i + 1)) - s1) < 1) {
+ i2 = Math.ceil(s2);
if (i2 == i1) v = __frequencies[i1] * range;
- else v = (s1 + (s2 - s1) * (s1 - i1)) * range;
+ else v = (__frequencies[i1] + (__frequencies[i2] - __frequencies[i1]) * (s1 - i1)) * range;
}
else {
v = __frequencies[i1] * (Math.ceil(s1) - i1);
- while (++i1 < i2) v += __frequencies[i1];
- v += __frequencies[i2] * (s2 - Math.floor(s2));
+ if (i1 != (i2 = Math.floor(s2))) {
+ while (++i1 < i2) v += __frequencies[i1];
+ v += __frequencies[i2] * (s2 - Math.floor(s2));
+ }
}
-
i1 = Math.floor(s1 = s2);
- if (v > 0) v = 20 * Math.log(v) / 2.302585092994046;//log(10)
- else v = -200;
-
- v = CoolUtil.bound(1 + v / dbRange, 0, 1);
-
- if (delta > 0 && levels[i] > v) levels[i] -= (levels[i] - v) * delta;
+ v = ((20 * Math.log(v) / 2.302585092994046) - minDb) / dbRange;
+ if (delta > 0 && delta < 1 && v < levels[i]) levels[i] -= Math.pow(levels[i] - v, 2) * delta;
else levels[i] = v;
}
@@ -194,6 +188,71 @@ class AudioAnalyzer {
* @return Output of frequencies ranging from 1 to fftN/2.
**/
public function getFrequencies(startPos:Float, ?frequencies:Array):Array {
+ // https://github.com/FunkinCrew/grig.audio/commit/8567c4dad34cfeaf2ff23fe12c3796f5db80685e
+ inline function butterfly4PointOptimized(i0:Int, i1:Int, i2:Int, i3:Int, w1_idx:Int, w2_idx:Int, w3_idx:Int) {
+ // Load input values
+ var x0r = __freqReals[i0];
+ var x0i = __freqImags[i0];
+
+ // Apply twiddle factors to x1, x2, x3
+ // x1 = workingData[i1] * twiddle1
+ var x1r_raw = __freqReals[i1];
+ var x1i_raw = __freqImags[i1];
+ var tw1r = __twiddleReals[w1_idx];
+ var tw1i = __twiddleImags[w1_idx];
+ var x1r = x1r_raw * tw1r - x1i_raw * tw1i;
+ var x1i = x1r_raw * tw1i + x1i_raw * tw1r;
+
+ // x2 = workingData[i2] * twiddle2
+ var x2r_raw = __freqReals[i2];
+ var x2i_raw = __freqImags[i2];
+ var tw2r = __twiddleReals[w2_idx];
+ var tw2i = __twiddleImags[w2_idx];
+ var x2r = x2r_raw * tw2r - x2i_raw * tw2i;
+ var x2i = x2r_raw * tw2i + x2i_raw * tw2r;
+
+ // x3 = workingData[i3] * twiddle3
+ var x3r_raw = __freqReals[i3];
+ var x3i_raw = __freqImags[i3];
+ var tw3r = __twiddleReals[w3_idx];
+ var tw3i = __twiddleImags[w3_idx];
+ var x3r = x3r_raw * tw3r - x3i_raw * tw3i;
+ var x3i = x3r_raw * tw3i + x3i_raw * tw3r;
+
+ // Compute intermediate values for 4-point DFT
+ var t0r = x0r + x2r; // (x0 + x2).real
+ var t0i = x0i + x2i; // (x0 + x2).imag
+ var t1r = x0r - x2r; // (x0 - x2).real
+ var t1i = x0i - x2i; // (x0 - x2).imag
+ var t2r = x1r + x3r; // (x1 + x3).real
+ var t2i = x1i + x3i; // (x1 + x3).imag
+ var t3r = x1r - x3r; // (x1 - x3).real
+ var t3i = x1i - x3i; // (x1 - x3).imag
+
+ // Apply j multiplication: j * (a + jb) = -b + ja
+ var jt3r = -t3i; // j * t3.real = -t3.imag
+ var jt3i = t3r; // j * t3.imag = t3.real
+
+ // Final 4-point DFT butterfly outputs
+ __freqReals[i0] = t0r + t2r; // X[k]
+ __freqImags[i0] = t0i + t2i;
+ __freqReals[i1] = t1r - jt3r; // X[k + N/4]
+ __freqImags[i1] = t1i - jt3i;
+ __freqReals[i2] = t0r - t2r; // X[k + N/2]
+ __freqImags[i2] = t0i - t2i;
+ __freqReals[i3] = t1r + jt3r; // X[k + 3N/4]
+ __freqImags[i3] = t1i + jt3i;
+ }
+
+ inline function butterfly2PointOptimized(i0:Int, i1:Int, w_idx:Int) {
+ var tempr = __freqReals[i1] * __twiddleReals[w_idx] - __freqImags[i1] * __twiddleImags[w_idx];
+ var tempi = __freqReals[i1] * __twiddleImags[w_idx] + __freqImags[i1] * __twiddleReals[w_idx];
+ __freqReals[i1] = __freqReals[i0] - tempr;
+ __freqImags[i1] = __freqImags[i0] - tempi;
+ __freqReals[i0] += tempr;
+ __freqImags[i0] += tempi;
+ }
+
__freqSamples = getSamples(startPos, fftN, true, __freqSamples);
if (frequencies == null) frequencies = [];
@@ -201,89 +260,26 @@ class AudioAnalyzer {
if (fftN == 1) frequencies[0] = __freqSamples[0];
else {
+ var n;
for (i in 0...fftN) {
- __freqReals[__reverseIndices[i]] = __freqSamples[i] * __windows[i];
- __freqImags[i] = 0;
+ n = __reverseIndices[i];
+ __freqReals[n] = __freqSamples[i] * __windows[i];
+ __freqImags[n] = 0;
}
- // https://github.com/FunkinCrew/grig.audio/commit/8567c4dad34cfeaf2ff23fe12c3796f5db80685e
- inline function butterfly4PointOptimized(i0:Int, i1:Int, i2:Int, i3:Int, w1_idx:Int, w2_idx:Int, w3_idx:Int) {
- // Load input values
- var x0r = __freqReals[i0];
- var x0i = __freqImags[i0];
-
- // Apply twiddle factors to x1, x2, x3
- // x1 = workingData[i1] * twiddle1
- var x1r_raw = __freqReals[i1];
- var x1i_raw = __freqImags[i1];
- var tw1r = __twiddleReals[w1_idx];
- var tw1i = __twiddleImags[w1_idx];
- var x1r = x1r_raw * tw1r - x1i_raw * tw1i;
- var x1i = x1r_raw * tw1i + x1i_raw * tw1r;
-
- // x2 = workingData[i2] * twiddle2
- var x2r_raw = __freqReals[i2];
- var x2i_raw = __freqImags[i2];
- var tw2r = __twiddleReals[w2_idx];
- var tw2i = __twiddleImags[w2_idx];
- var x2r = x2r_raw * tw2r - x2i_raw * tw2i;
- var x2i = x2r_raw * tw2i + x2i_raw * tw2r;
-
- // x3 = workingData[i3] * twiddle3
- var x3r_raw = __freqReals[i3];
- var x3i_raw = __freqImags[i3];
- var tw3r = __twiddleReals[w3_idx];
- var tw3i = __twiddleImags[w3_idx];
- var x3r = x3r_raw * tw3r - x3i_raw * tw3i;
- var x3i = x3r_raw * tw3i + x3i_raw * tw3r;
-
- // Compute intermediate values for 4-point DFT
- var t0r = x0r + x2r; // (x0 + x2).real
- var t0i = x0i + x2i; // (x0 + x2).imag
- var t1r = x0r - x2r; // (x0 - x2).real
- var t1i = x0i - x2i; // (x0 - x2).imag
- var t2r = x1r + x3r; // (x1 + x3).real
- var t2i = x1i + x3i; // (x1 + x3).imag
- var t3r = x1r - x3r; // (x1 - x3).real
- var t3i = x1i - x3i; // (x1 - x3).imag
-
- // Apply j multiplication: j * (a + jb) = -b + ja
- var jt3r = -t3i; // j * t3.real = -t3.imag
- var jt3i = t3r; // j * t3.imag = t3.real
-
- // Final 4-point DFT butterfly outputs
- __freqReals[i0] = t0r + t2r; // X[k]
- __freqImags[i0] = t0i + t2i;
- __freqReals[i1] = t1r - jt3r; // X[k + N/4]
- __freqImags[i1] = t1i - jt3i;
- __freqReals[i2] = t0r - t2r; // X[k + N/2]
- __freqImags[i2] = t0i - t2i;
- __freqReals[i3] = t1r + jt3r; // X[k + 3N/4]
- __freqImags[i3] = t1i + jt3i;
- }
-
- inline function butterfly2PointOptimized(i0:Int, i1:Int, w_idx:Int) {
- var tempr = __freqReals[i1] * __twiddleReals[w_idx] - __freqImags[i1] * __twiddleImags[w_idx];
- var tempi = __freqReals[i1] * __twiddleImags[w_idx] + __freqImags[i1] * __twiddleReals[w_idx];
- __freqReals[i1] = __freqReals[i0] - tempr;
- __freqImags[i1] = __freqImags[i0] - tempi;
- __freqReals[i0] += tempr;
- __freqImags[i0] += tempi;
- }
-
- var size = 1, n, s, start, t;
+ var size = 1, s2, start, t;
for (radix in __factors) {
n = Math.floor(fftN / (size *= radix));
- s = size >> (radix >> 1);
+ s2 = size >> (radix >> 1);
if (radix == 4) for (i in 0...n) {
start = i * size;
- for (k in 0...s)
- butterfly4PointOptimized(t = start + k, t = (t + s), t = (t + s), t = (t + s),
+ for (k in 0...s2)
+ butterfly4PointOptimized(t = start + k, t = (t + s2), t = (t + s2), t = (t + s2),
(k * n) % fftN, (2 * k * n) % fftN, (3 * k * n) % fftN);
}
else for (i in 0...n) {
start = i * size;
- for (k in 0...s) butterfly2PointOptimized(t = start + k, t = (t + s), (k * n) % fftN);
+ for (k in 0...s2) butterfly2PointOptimized(t = start + k, t = (t + s2), (k * n) % fftN);
}
}
@@ -344,7 +340,7 @@ class AudioAnalyzer {
* @return Output of
**/
public function getSamples(startPos:Float, length:Int, mono = true, ?output:Array):Array {
- ((output == null) ? (__sampleOutput = output = []) : (__sampleOutput = output)).resize((__sampleOutputLength = length) * (mono ? 1 : buffer.channels));
+ ((output == null) ? (__sampleOutput = output = []) : (__sampleOutput = output)).resize(__sampleOutputLength = length * (mono ? 1 : buffer.channels));
__sampleIndex = 0;
__check();
@@ -354,7 +350,6 @@ class AudioAnalyzer {
return output;
}
- // TODO: check later if this formula is correct.
function __getSamplesCallbackMerge(b:Int, c:Int):Void if (__sampleIndex < __sampleOutputLength) {
if (c == 0) __sampleOutput[__sampleIndex] = b / buffer.channels / byteSize;
else if (c == buffer.channels) {
From f3f517d0a8d4c11d2b104476d8c66a8c59df2f3a Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Fri, 1 Aug 2025 15:25:26 +0700
Subject: [PATCH 10/11] Disable 4 factoring, it looks ass with it
---
source/funkin/backend/utils/AudioAnalyzer.hx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index 2eeedb6b..42db432c 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -117,10 +117,10 @@ class AudioAnalyzer {
__factors.resize(0);
var inv = fftN;
- while (inv % 4 == 0) {
+ /*while (inv % 4 == 0) {
__factors.push(4);
inv >>= 2;
- }
+ }*/
while (inv % 2 == 0) {
__factors.push(2);
From e0a26fdbd2c32b60f35ffabafc2fdb34f0ce5fc8 Mon Sep 17 00:00:00 2001
From: Ralty <78720179+Raltyro@users.noreply.github.com>
Date: Fri, 1 Aug 2025 15:45:39 +0700
Subject: [PATCH 11/11] Documentation for AudioAnalyzer
---
source/funkin/backend/utils/AudioAnalyzer.hx | 82 +++++++++++++++-----
1 file changed, 61 insertions(+), 21 deletions(-)
diff --git a/source/funkin/backend/utils/AudioAnalyzer.hx b/source/funkin/backend/utils/AudioAnalyzer.hx
index 42db432c..3f552c55 100644
--- a/source/funkin/backend/utils/AudioAnalyzer.hx
+++ b/source/funkin/backend/utils/AudioAnalyzer.hx
@@ -18,23 +18,48 @@ typedef AudioAnalyzerCallback = Int->Int->Void;
*
* FlxSound.amplitude does work in CNE so if any case if your only checking for peak of current
* time, use that instead.
-**/
+ */
class AudioAnalyzer {
- public static function getByte(buf:ArrayBuffer, pos:Int, wordSize:Int):Int {
- if (wordSize == 2) return inline ArrayBufferIO.getInt16(buf, pos);
+ /**
+ * 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).
+ * @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);
else if (wordSize == 3) {
- var b = inline ArrayBufferIO.getUint16(buf, pos) | (buf.get(pos + 2) << 16);
+ var b = inline ArrayBufferIO.getUint16(buffer, position) | (buffer.get(position + 2) << 16);
if (b & 0x800000 != 0) return b - 0x1000000;
else return b;
}
- else if (wordSize == 4) return inline ArrayBufferIO.getInt32(buf, pos);
- else return inline ArrayBufferIO.getUint8(buf, pos) - 128;
+ else if (wordSize == 4) return inline ArrayBufferIO.getInt32(buffer, position);
+ else return inline ArrayBufferIO.getUint8(buffer, position) - 128;
}
+ /**
+ * The current sound to analyze.
+ */
public var sound:FlxSound;
- public var buffer(default, null):AudioBuffer;
+
+ /**
+ * How much samples for the fft to get.
+ * Usually for getting the levels or frequencies of the sound.
+ *
+ * Has to be power of two, or it won't work.
+ */
public var fftN(default, set):Int;
+ /**
+ * 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)
+ */
public var byteSize(default, null):Int;
var __toBits:Float;
@@ -76,8 +101,9 @@ class AudioAnalyzer {
/**
* Creates an analyzer for specified FlxSound
* @param sound An FlxSound to analyze.
- **/
- public function new(sound:FlxSound, fftN = 512) {
+ * @param fftN How much samples for fft to get (Optional, default 2048).
+ */
+ public function new(sound:FlxSound, fftN = 2048) {
this.sound = sound;
this.fftN = fftN;
__check();
@@ -146,7 +172,19 @@ class AudioAnalyzer {
return y;
}
- public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, minDb = -70, maxDb = -10, minFreq = 20.0, maxFreq = 22000.0):Array {
+ /**
+ * Gets levels from an attached FlxSound from startPos, basically a minimized of frequencies.
+ * @param startPos Start Position to get from sound in milliseconds.
+ * @param barCount How much bars to get.
+ * @param levels The output for getting the values, to avoid memory leaks (Optional).
+ * @param delta How much delta for smoothen the values from the previous levels values (Optional).
+ * @param minDb The minimum decibels to cap (Optional, default -70.0).
+ * @param maxDb The maximum decibels to cap (Optional, default -10.0).
+ * @param minFreq The minimum frequency to cap (Optional, default 20.0).
+ * @param maxFreq The maximum frequency to cap (Optional, default 22000.0).
+ * @return Output of levels/bars
+ */
+ public function getLevels(startPos:Float, barCount:Int, ?levels:Array, delta = 0.0, minDb = -70.0, maxDb = -10.0, minFreq = 20.0, maxFreq = 22000.0):Array {
__frequencies = getFrequencies(startPos, __frequencies);
if (levels == null) levels = [];
@@ -184,9 +222,11 @@ class AudioAnalyzer {
}
/**
- * Gets frequencies from an attached FlxSound from startPos with samples
- * @return Output of frequencies ranging from 1 to fftN/2.
- **/
+ * Gets frequencies from an attached FlxSound from startPos.
+ * @param startPos Start Position to get from sound in milliseconds.
+ * @param frequencies The output for getting the frequencies, to avoid memory leaks (Optional).
+ * @return Output of frequencies
+ */
public function getFrequencies(startPos:Float, ?frequencies:Array):Array {
// https://github.com/FunkinCrew/grig.audio/commit/8567c4dad34cfeaf2ff23fe12c3796f5db80685e
inline function butterfly4PointOptimized(i0:Int, i1:Int, i2:Int, i3:Int, w1_idx:Int, w2_idx:Int, w3_idx:Int) {
@@ -293,12 +333,12 @@ class AudioAnalyzer {
/**
* Analyzes an attached FlxSound from startPos to endPos in milliseconds to get the amplitudes.
- * @param startPos Start Position of the FlxSound in milliseconds.
- * @param endPos End Position of the FlxSound in milliseconds.
+ * @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;
@@ -333,12 +373,12 @@ class AudioAnalyzer {
/**
* Gets samples from startPos with given length of samples.
- * @param startPos Start Position of the FlxSound in milliseconds.
+ * @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 Output that gets passed into this function (Optional).
* @return Output of
- **/
+ */
public function getSamples(startPos:Float, length:Int, mono = true, ?output:Array):Array {
((output == null) ? (__sampleOutput = output = []) : (__sampleOutput = output)).resize(__sampleOutputLength = length * (mono ? 1 : buffer.channels));
__sampleIndex = 0;
@@ -367,10 +407,10 @@ class AudioAnalyzer {
/**
* Read an attached FlxSound from startPos to endPos in milliseconds with a callback.
- * @param startPos Start Position of the FlxSound in milliseconds.
- * @param endPos End Position of the FlxSound in milliseconds.
+ * @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.
- **/
+ */
public function read(startPos:Float, endPos:Float, callback:AudioAnalyzerCallback) {
__check();
__read(startPos, endPos, callback);