1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-22 18:09:33 +00:00

audiovis reorganize + MeshRender obj in progres

This commit is contained in:
Cameron Taylor 2022-02-10 13:17:46 -05:00
parent 96540fc09e
commit c73e8e1a4d
10 changed files with 217 additions and 102 deletions

View file

@ -1,5 +1,6 @@
package;
import audiovis.SpectogramSprite;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;

View file

@ -1,4 +1,6 @@
import dsp.FFT;
package audiovis;
import audiovis.dsp.FFT;
import flixel.FlxSprite;
import flixel.addons.plugin.taskManager.FlxTask;
import flixel.graphics.frames.FlxAtlasFrames;

View file

@ -1,6 +1,6 @@
package;
package audiovis;
import dsp.FFT;
import audiovis.dsp.FFT;
import flixel.FlxSprite;
import flixel.group.FlxGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;

View file

@ -1,6 +1,6 @@
package;
package audiovis;
import dsp.FFT;
import audiovis.dsp.FFT;
import flixel.math.FlxMath;
import flixel.system.FlxSound;
import lime.utils.Int16Array;

View file

@ -1,4 +1,4 @@
package dsp;
package audiovis.dsp;
/**
Complex number representation.

View file

@ -1,16 +1,17 @@
package dsp;
package audiovis.dsp;
import dsp.Complex;
import audiovis.dsp.Complex;
using audiovis.dsp.OffsetArray;
using audiovis.dsp.Signal;
// these are only used for testing, down in FFT.main()
using dsp.OffsetArray;
using dsp.Signal;
/**
Fast/Finite Fourier Transforms.
**/
class FFT {
class FFT
{
/**
Computes the Discrete Fourier Transform (DFT) of a `Complex` sequence.
@ -19,7 +20,7 @@ class FFT {
samples from the Discrete-Time Fourier Transform (DTFT) - which is Fs-periodic -
with a spacing of Fs/N Hz between them and a scaling factor of Fs.
**/
public static function fft(input:Array<Complex>) : Array<Complex>
public static function fft(input:Array<Complex>):Array<Complex>
return do_fft(input, false);
/**
@ -28,7 +29,8 @@ class FFT {
Since the input time signal is real, its frequency representation is
Hermitian-symmetric so we only return the positive frequencies.
**/
public static function rfft(input:Array<Float>) : Array<Complex> {
public static function rfft(input:Array<Float>):Array<Complex>
{
final s = fft(input.map(Complex.fromReal));
return s.slice(0, Std.int(s.length / 2) + 1);
}
@ -40,11 +42,12 @@ class FFT {
from each other, the result will consist of N data points as sampled
from a time signal at intervals of 1/Fs with a scaling factor of 1/Fs.
**/
public static function ifft(input:Array<Complex>) : Array<Complex>
public static function ifft(input:Array<Complex>):Array<Complex>
return do_fft(input, true);
// Handles padding and scaling for forwards and inverse FFTs.
private static function do_fft(input:Array<Complex>, inverse:Bool) : Array<Complex> {
private static function do_fft(input:Array<Complex>, inverse:Bool):Array<Complex>
{
final n = nextPow2(input.length);
var ts = [for (i in 0...n) if (i < input.length) input[i] else Complex.zero];
var fs = [for (_ in 0...n) Complex.zero];
@ -54,36 +57,41 @@ class FFT {
}
// Radix-2 Decimation-In-Time variant of CooleyTukey's FFT, recursive.
private static function ditfft2(
time:Array<Complex>, t:Int,
freq:Array<Complex>, f:Int,
n:Int, step:Int, inverse: Bool
) : Void {
if (n == 1) {
private static function ditfft2(time:Array<Complex>, t:Int, freq:Array<Complex>, f:Int, n:Int, step:Int, inverse:Bool):Void
{
if (n == 1)
{
freq[f] = time[t].copy();
} else {
}
else
{
final halfLen = Std.int(n / 2);
ditfft2(time, t, freq, f, halfLen, step * 2, inverse);
ditfft2(time, t, freq, f, halfLen, step * 2, inverse);
ditfft2(time, t + step, freq, f + halfLen, halfLen, step * 2, inverse);
for (k in 0...halfLen) {
for (k in 0...halfLen)
{
final twiddle = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k / n);
final even = freq[f + k].copy();
final odd = freq[f + k + halfLen].copy();
freq[f + k] = even + twiddle * odd;
freq[f + k] = even + twiddle * odd;
freq[f + k + halfLen] = even - twiddle * odd;
}
}
}
// Naive O(n^2) DFT, used for testing purposes.
private static function dft(ts:Array<Complex>, ?inverse:Bool) : Array<Complex> {
if (inverse == null) inverse = false;
private static function dft(ts:Array<Complex>, ?inverse:Bool):Array<Complex>
{
if (inverse == null)
inverse = false;
final n = ts.length;
var fs = new Array<Complex>();
fs.resize(n);
for (f in 0...n) {
for (f in 0...n)
{
var sum = Complex.zero;
for (t in 0...n) {
for (t in 0...n)
{
sum += ts[t] * Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * f * t / n);
}
fs[f] = inverse ? sum.scale(1 / n) : sum;
@ -94,17 +102,22 @@ class FFT {
/**
Finds the power of 2 that is equal to or greater than the given natural.
**/
static function nextPow2(x:Int) : Int {
if (x < 2) return 1;
else if ((x & (x-1)) == 0) return x;
static function nextPow2(x:Int):Int
{
if (x < 2)
return 1;
else if ((x & (x - 1)) == 0)
return x;
var pow = 2;
x--;
while ((x >>= 1) != 0) pow <<= 1;
while ((x >>= 1) != 0)
pow <<= 1;
return pow;
}
// testing, but also acts like an example
static function main() {
static function main()
{
// sampling and buffer parameters
final Fs = 44100.0;
final N = 512;
@ -116,39 +129,39 @@ class FFT {
// get positive spectrum and use its symmetry to reconstruct negative domain
final fs_pos = rfft(ts);
final fs_fft = new OffsetArray(
[for (k in -(halfN - 1) ... 0) fs_pos[-k].conj()].concat(fs_pos),
-(halfN - 1)
);
final fs_fft = new OffsetArray([for (k in -(halfN - 1)...0) fs_pos[-k].conj()].concat(fs_pos), -(halfN - 1));
// double-check with naive DFT
final fs_dft = new OffsetArray(
dft(ts.map(Complex.fromReal)).circShift(halfN - 1),
-(halfN - 1)
);
final fs_err = [for (k in -(halfN - 1) ... halfN) fs_fft[k] - fs_dft[k]];
final fs_dft = new OffsetArray(dft(ts.map(Complex.fromReal)).circShift(halfN - 1), -(halfN - 1));
final fs_err = [for (k in -(halfN - 1)...halfN) fs_fft[k] - fs_dft[k]];
final max_fs_err = fs_err.map(z -> z.magnitude).max();
if (max_fs_err > 1e-6) haxe.Log.trace('FT Error: ${max_fs_err}', null);
if (max_fs_err > 1e-6)
haxe.Log.trace('FT Error: ${max_fs_err}', null);
// else for (k => s in fs_fft) haxe.Log.trace('${k * Fs / N};${s.scale(1 / Fs).magnitude}', null);
// find spectral peaks to detect signal frequencies
final freqis = fs_fft.array.map(z -> z.magnitude)
.findPeaks()
.map(k -> (k - (halfN - 1)) * Fs / N)
.filter(f -> f >= 0);
if (freqis.length != freqs.length) {
.findPeaks()
.map(k -> (k - (halfN - 1)) * Fs / N)
.filter(f -> f >= 0);
if (freqis.length != freqs.length)
{
trace('Found frequencies: ${freqis}');
} else {
}
else
{
final freqs_err = [for (i in 0...freqs.length) freqis[i] - freqs[i]];
final max_freqs_err = freqs_err.map(Math.abs).max();
if (max_freqs_err > Fs / N) trace('Frequency Errors: ${freqs_err}');
if (max_freqs_err > Fs / N)
trace('Frequency Errors: ${freqs_err}');
}
// recover time signal from the frequency domain
final ts_ifft = ifft(fs_fft.array.circShift(-(halfN - 1)).map(z -> z.scale(1 / Fs)));
final ts_err = [for (n in 0...N) ts_ifft[n].scale(Fs).real - ts[n]];
final max_ts_err = ts_err.map(Math.abs).max();
if (max_ts_err > 1e-6) haxe.Log.trace('IFT Error: ${max_ts_err}', null);
if (max_ts_err > 1e-6)
haxe.Log.trace('IFT Error: ${max_ts_err}', null);
// else for (n in 0...ts_ifft.length) haxe.Log.trace('${n / Fs};${ts_ifft[n].scale(Fs).real}', null);
}
}

View file

@ -1,4 +1,4 @@
package dsp;
package audiovis.dsp;
/**
A view into an Array with an indexing offset.
@ -7,28 +7,30 @@ package dsp;
**/
@:forward(array, offset)
abstract OffsetArray<T>({
final array : Array<T>;
final offset : Int;
}) {
final array:Array<T>;
final offset:Int;
})
{
public inline function new(array:Array<T>, offset:Int)
this = { array: array, offset: offset };
this = {array: array, offset: offset};
public var length(get, never):Int;
public var length(get,never) : Int;
inline function get_length()
return this.array.length;
@:arrayAccess
public inline function get(index:Int) : T
public inline function get(index:Int):T
return this.array[index - this.offset];
@:arrayAccess
public inline function set(index:Int, value:T) : Void
public inline function set(index:Int, value:T):Void
this.array[index - this.offset] = value;
/**
Iterates through items in their original order while providing the altered indexes as keys.
**/
public inline function keyValueIterator() : KeyValueIterator<Int,T>
public inline function keyValueIterator():KeyValueIterator<Int, T>
return new OffsetArrayIterator(this.array, this.offset);
@:from
@ -44,35 +46,42 @@ abstract OffsetArray<T>({
same order but shifted by `n` positions (to the right if positive and to
the left if negative) in **circular** fashion (no elements discarded).
**/
public static function circShift<T>(array:Array<T>, n:Int) : Array<T> {
if (n < 0) return circShift(array, array.length + n);
public static function circShift<T>(array:Array<T>, n:Int):Array<T>
{
if (n < 0)
return circShift(array, array.length + n);
var shifted = new Array<T>();
n = n % array.length;
for (i in array.length - n ... array.length) shifted.push(array[i]);
for (i in 0 ... array.length - n) shifted.push(array[i]);
for (i in array.length - n...array.length)
shifted.push(array[i]);
for (i in 0...array.length - n)
shifted.push(array[i]);
return shifted;
}
}
private class OffsetArrayIterator<T> {
private final array : Array<T>;
private final offset : Int;
private var enumeration : Int;
private class OffsetArrayIterator<T>
{
private final array:Array<T>;
private final offset:Int;
private var enumeration:Int;
public inline function new(array:Array<T>, offset:Int) {
public inline function new(array:Array<T>, offset:Int)
{
this.array = array;
this.offset = offset;
this.enumeration = 0;
}
public inline function next() : {key:Int, value:T} {
public inline function next():{key:Int, value:T}
{
final i = this.enumeration++;
return { key: i + this.offset, value: this.array[i] };
return {key: i + this.offset, value: this.array[i]};
}
public inline function hasNext() : Bool
public inline function hasNext():Bool
return this.enumeration < this.array.length;
}

View file

@ -1,24 +1,31 @@
package dsp;
package audiovis.dsp;
using Lambda;
/**
Signal processing miscellaneous utilities.
**/
class Signal {
class Signal
{
/**
Returns a smoothed version of the input array using a moving average.
**/
public static function smooth(y:Array<Float>, n:Int) : Array<Float> {
if (n <= 0) {
public static function smooth(y:Array<Float>, n:Int):Array<Float>
{
if (n <= 0)
{
return null;
} else if (n == 1) {
}
else if (n == 1)
{
return y.copy();
} else {
}
else
{
var smoothed = new Array<Float>();
smoothed.resize(y.length);
for (i in 0...y.length) {
for (i in 0...y.length)
{
var m = i + 1 < n ? i : n - 1;
smoothed[i] = sum(y.slice(i - m, i + 1));
}
@ -32,23 +39,19 @@ class Signal {
@param threshold Minimal peak height wrt. its neighbours, defaults to 0.
@param minHeight Minimal peak height wrt. the whole input, defaults to global minimum.
**/
public static function findPeaks(
y:Array<Float>,
?threshold:Float,
?minHeight:Float
) : Array<Int> {
public static function findPeaks(y:Array<Float>, ?threshold:Float, ?minHeight:Float):Array<Int>
{
threshold = threshold == null ? 0.0 : Math.abs(threshold);
minHeight = minHeight == null ? Signal.min(y) : minHeight;
var peaks = new Array<Int>();
final dy = [for (i in 1...y.length) y[i] - y[i-1]];
for (i in 1...dy.length) {
final dy = [for (i in 1...y.length) y[i] - y[i - 1]];
for (i in 1...dy.length)
{
// peak: function growth positive to its left and negative to its right
if (
dy[i-1] > threshold && dy[i] < -threshold &&
y[i] > minHeight
) {
if (dy[i - 1] > threshold && dy[i] < -threshold && y[i] > minHeight)
{
peaks.push(i);
}
}
@ -61,17 +64,18 @@ class Signal {
This function tries to minimize floating-point precision errors.
**/
public static function sum(array:Array<Float>) : Float {
public static function sum(array:Array<Float>):Float
{
// Neumaier's "improved Kahan-Babuska algorithm":
var sum = 0.0;
var c = 0.0; // running compensation for lost precision
for (v in array) {
for (v in array)
{
var t = sum + v;
c += Math.abs(sum) >= Math.abs(v)
? (sum - t) + v // sum is bigger => low-order digits of v are lost
: (v - t) + sum; // v is bigger => low-order digits of sum are lost
c += Math.abs(sum) >= Math.abs(v) ? (sum - t) + v // sum is bigger => low-order digits of v are lost
: (v - t) + sum; // v is bigger => low-order digits of sum are lost
sum = t;
}
@ -81,30 +85,30 @@ class Signal {
/**
Returns the average value of an array.
**/
public static function mean(y:Array<Float>) : Float
public static function mean(y:Array<Float>):Float
return sum(y) / y.length;
/**
Returns the global maximum.
**/
public static function max(y:Array<Float>) : Float
public static function max(y:Array<Float>):Float
return y.fold(Math.max, y[0]);
/**
Returns the global maximum's index.
**/
public static function maxi(y:Array<Float>) : Int
public static function maxi(y:Array<Float>):Int
return y.foldi((yi, m, i) -> yi > y[m] ? i : m, 0);
/**
Returns the global minimum.
**/
public static function min(y:Array<Float>) : Float
public static function min(y:Array<Float>):Float
return y.fold(Math.min, y[0]);
/**
Returns the global minimum's index.
**/
public static function mini(y:Array<Float>) : Int
public static function mini(y:Array<Float>):Int
return y.foldi((yi, m, i) -> yi < y[m] ? i : m, 0);
}

View file

@ -4,6 +4,8 @@ import Conductor.BPMChangeEvent;
import Note.NoteData;
import Section.SwagSection;
import SongLoad.SwagSong;
import audiovis.ABotVis;
import audiovis.SpectogramSprite;
import flixel.FlxSprite;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
@ -26,6 +28,7 @@ import lime.utils.Assets;
import openfl.events.Event;
import openfl.events.IOErrorEvent;
import openfl.net.FileReference;
import rendering.MeshRender;
using Lambda;
using StringTools;

View file

@ -0,0 +1,83 @@
package rendering;
import flixel.FlxStrip;
/**
* Yoinked from AustinEast, thanks hopefully u dont mind me using some of ur good code
* instead of my dumbass ugly code bro
*/
class MeshRender extends FlxStrip
{
public var vertex_count(default, null):Int = 0;
public var index_count(default, null):Int = 0;
var tri_offset:Int = 0;
public function new(x, y)
{
super(x, y);
makeGraphic(1, 1);
}
public inline function start()
{
tri_offset = vertex_count;
}
public inline function add_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0)
{
final pos = vertex_count << 1;
vertices[pos] = x;
vertices[pos + 1] = y;
uvtData[pos] = u;
uvtData[pos + 1] = v;
vertex_count++;
}
public function add_tri(a:Int, b:Int, c:Int)
{
indices[index_count] = a + tri_offset;
indices[index_count + 1] = b + tri_offset;
indices[index_count + 2] = c + tri_offset;
index_count += 3;
}
/**
*
* top left - a
*
* top right - b
*
* bottom left - c
*
* bottom right - d
*/
public function add_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0,
bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0)
{
start();
// top left
add_vertex(bx, by, bu, bv);
// top right
add_vertex(ax, ay, au, av);
// bottom left
add_vertex(cx, cy, cu, cv);
// bottom right
add_vertex(dx, dy, du, dv);
add_tri(0, 1, 2);
add_tri(0, 2, 3);
}
public function clear()
{
vertices.length = 0;
indices.length = 0;
vertex_count = 0;
index_count = 0;
}
}