├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── LICENSE.md
├── README.md
├── example
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── Project.xml
├── assets
│ ├── data
│ │ └── data-goes-here.txt
│ ├── images
│ │ └── images-go-here.txt
│ ├── music
│ │ ├── catStuck.ogg
│ │ ├── medlyForAGamer.ogg
│ │ ├── music-goes-here.txt
│ │ └── shoreline.ogg
│ └── sounds
│ │ └── sounds-go-here.txt
├── hmm.json
├── hxformat.json
└── source
│ ├── AssetPaths.hx
│ ├── Main.hx
│ ├── PlayState.hx
│ └── Visualizer.hx
├── haxelib.json
├── hmm.json
└── src
└── funkin
└── vis
├── AudioBuffer.hx
├── AudioClip.hx
├── LogHelper.hx
├── Scaling.hx
├── _internal
└── html5
│ └── AnalyzerNode.hx
├── audioclip
└── frontends
│ └── LimeAudioClip.hx
└── dsp
├── Complex.hx
├── FFT.hx
├── OffsetArray.hx
├── README.md
├── RecentPeakFinder.hx
├── Signal.hx
└── SpectralAnalyzer.hx
/.gitignore:
--------------------------------------------------------------------------------
1 | export/
2 | .haxelib/
3 | assets/music/copyrightlol
4 | .DS_Store
5 | secret/
6 | bin/
7 | build/
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Build + Debug",
6 | "type": "lime",
7 | "request": "launch"
8 | },
9 | {
10 | "name": "Debug",
11 | "type": "lime",
12 | "request": "launch",
13 | "preLaunchTask": null
14 | },
15 | {
16 | "name": "Macro",
17 | "type": "haxe-eval",
18 | "request": "launch"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "lime",
6 | "command": "test",
7 | "group": {
8 | "kind": "build",
9 | "isDefault": true
10 | }
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 The Funkin' Crew Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # funkin.vis
2 |
3 | `funkin.vis` is a haxelib for processing audio data into frequency data using FFT's, created for Friday Night Funkin' by The Funkin' Crew Inc.
4 |
5 | On web it uses web browsers' `AnalyzerNode`, and on native it uses a Radix-2 FFT algorithm.
6 |
7 | ## Installation
8 |
9 | `haxelib git funkin.vis https://github.com/FunkinCrew/funkVis`
10 |
11 | ## Usage
12 |
13 | - todo...
14 |
15 |
16 | ## LICENSE
17 |
18 | `funkin.vis` is licensed under the MIT license, which can be viewed here: [LICENSE.md](/LICENSE.md)
19 |
20 | ### Attributions
21 |
22 | In the [`example/assets/music`](example/assets/music) folder are a handful of songs for demonstration use.
23 | Below is the attribution links, and relavent licensing information.
24 |
25 | | Musician | Song | License |
26 | | --- | --- | --- |
27 | | [baryiscool][] | [Cat stuck in an elevator][] | [CC BY 3.0][] |
28 | | [AlbeGian][] | [Medley for a Gamer][] | [CC BY 3.0][] |
29 |
30 |
31 |
32 | [baryiscool]: https://baryiscool.newgrounds.com
33 | [AlbeGian]: https://AlbeGian.newgrounds.com
34 |
35 |
36 | [Cat stuck in an elevator]: https://www.newgrounds.com/audio/listen/1294925
37 | [Medley for a Gamer]: https://www.newgrounds.com/audio/listen/1286411
38 |
39 |
40 | [CC BY 3.0]: https://creativecommons.org/licenses/by/3.0/legalcode
--------------------------------------------------------------------------------
/example/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "openfl.lime-vscode-extension",
4 | "redhat.vscode-xml"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/example/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Build + Debug",
6 | "type": "lime",
7 | "request": "launch"
8 | },
9 | {
10 | "name": "Debug",
11 | "type": "lime",
12 | "request": "launch",
13 | "preLaunchTask": null
14 | },
15 | {
16 | "name": "Macro",
17 | "type": "haxe-eval",
18 | "request": "launch"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/example/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.exclude": {
3 | "export/**/*.hx": true
4 | },
5 | "[haxe]": {
6 | "editor.formatOnSave": true,
7 | "editor.formatOnPaste": true,
8 | "editor.codeActionsOnSave": {
9 | "source.sortImports": "always"
10 | }
11 | },
12 | "haxe.enableExtendedIndentation": true
13 | }
--------------------------------------------------------------------------------
/example/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "lime",
6 | "command": "test",
7 | "group": {
8 | "kind": "build",
9 | "isDefault": true
10 | }
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/example/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/example/assets/data/data-goes-here.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/data/data-goes-here.txt
--------------------------------------------------------------------------------
/example/assets/images/images-go-here.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/images/images-go-here.txt
--------------------------------------------------------------------------------
/example/assets/music/catStuck.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/music/catStuck.ogg
--------------------------------------------------------------------------------
/example/assets/music/medlyForAGamer.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/music/medlyForAGamer.ogg
--------------------------------------------------------------------------------
/example/assets/music/music-goes-here.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/music/music-goes-here.txt
--------------------------------------------------------------------------------
/example/assets/music/shoreline.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/music/shoreline.ogg
--------------------------------------------------------------------------------
/example/assets/sounds/sounds-go-here.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunkinCrew/funkVis/1966f8fbbbc509ed90d4b520f3c49c084fc92fd6/example/assets/sounds/sounds-go-here.txt
--------------------------------------------------------------------------------
/example/hmm.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": [
3 | {
4 | "name": "flixel",
5 | "type": "haxelib",
6 | "version": "5.3.1"
7 | },
8 | {
9 | "name": "funkin.vis",
10 | "path": "../",
11 | "type": "dev"
12 | },
13 | {
14 | "name": "grig.audio",
15 | "type": "git",
16 | "dir": "src",
17 | "ref": "cbf91e2180fd2e374924fe74844086aab7891666",
18 | "url": "https://gitlab.com/haxe-grig/grig.audio.git"
19 | },
20 | {
21 | "name": "hxcpp",
22 | "type": "haxelib",
23 | "version": "4.3.2"
24 | },
25 | {
26 | "name": "hxcpp-debug-server",
27 | "type": "haxelib",
28 | "version": "1.2.4"
29 | },
30 | {
31 | "name": "instrument",
32 | "type": "git",
33 | "dir": null,
34 | "ref": "master",
35 | "url": "https://github.com/AlexHaxe/haxe-instrument"
36 | },
37 | {
38 | "name": "lime",
39 | "type": "haxelib",
40 | "version": "8.0.2"
41 | },
42 | {
43 | "name": "openfl",
44 | "type": "haxelib",
45 | "version": "9.2.2"
46 | }
47 | ]
48 | }
--------------------------------------------------------------------------------
/example/hxformat.json:
--------------------------------------------------------------------------------
1 | {
2 | "lineEnds": {
3 | "leftCurly": "both",
4 | "rightCurly": "both",
5 | "objectLiteralCurly": {
6 | "leftCurly": "after"
7 | }
8 | },
9 | "sameLine": {
10 | "ifElse": "next",
11 | "doWhile": "next",
12 | "tryBody": "next",
13 | "tryCatch": "next"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/source/AssetPaths.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | @:build(flixel.system.FlxAssets.buildFileReferences("assets", true))
4 | class AssetPaths {}
5 |
--------------------------------------------------------------------------------
/example/source/Main.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxGame;
4 | import openfl.display.Sprite;
5 | import openfl.display.FPS;
6 |
7 | class Main extends Sprite
8 | {
9 | public function new()
10 | {
11 | super();
12 | addChild(new FlxGame(0, 0, PlayState, 144, 144));
13 | addChild(new FPS(5, 5, 0xFFFFFFFF));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/source/PlayState.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxG;
4 | import flixel.FlxState;
5 | import flixel.text.FlxText;
6 | import haxe.io.BytesInput;
7 | import haxe.io.Input;
8 | import haxe.io.UInt16Array;
9 | import lime.media.AudioSource;
10 | import lime.media.vorbis.VorbisFile;
11 | import lime.utils.Int16Array;
12 | import openfl.utils.Assets;
13 |
14 | using StringTools;
15 |
16 | class PlayState extends FlxState
17 | {
18 | var musicSrc:AudioSource;
19 | var data:lime.utils.UInt16Array;
20 |
21 | var debugText:FlxText;
22 |
23 | var musicList:Array = [];
24 |
25 | override public function create()
26 | {
27 | super.create();
28 |
29 | // musicList = fillMusicList("assets/music/musicList.txt");
30 | FlxG.sound.playMusic("assets/music/catStuck.ogg");
31 |
32 | @:privateAccess
33 | musicSrc = cast FlxG.sound.music._channel.__source;
34 |
35 | data = cast musicSrc.buffer.data;
36 |
37 | var visualizer = new Visualizer(musicSrc);
38 | add(visualizer);
39 |
40 | debugText = new FlxText(0, 0, 0, "test", 24);
41 | // add(debugText);
42 | }
43 |
44 | var max:Float = 0;
45 |
46 | override public function update(elapsed:Float)
47 | {
48 | var curIndex = Math.floor(musicSrc.buffer.sampleRate * (FlxG.sound.music.time / 1000));
49 | // trace(curIndex / (data.length * 2));
50 | // trace(FlxG.sound.music.time / FlxG.sound.music.length);
51 | // max = Math.max(max, data[curIndex]);
52 | debugText.text = "";
53 | // refactor below code to use addDebugText function
54 | // addDebugText(max / 2);
55 | // addDebugText(musicSrc.buffer.sampleRate);
56 | // addDebugText(data[curIndex]);
57 | // addDebugText(FlxG.sound.music.time / FlxG.sound.music.length);
58 | // addDebugText(curIndex / (data.length / 4));
59 | // addDebugText((data.length / 4) / musicSrc.buffer.sampleRate);
60 | // addDebugText(FlxG.sound.music.length / 1000);
61 | super.update(elapsed);
62 |
63 | if (FlxG.keys.justPressed.SPACE)
64 | {
65 | #if instrument
66 | // instrument.coverage.Coverage.endCoverage(); // when measuring coverage
67 | instrument.profiler.Profiler.endProfiler(); // when profiling
68 | #end
69 | }
70 | }
71 |
72 | function addDebugText(text:Dynamic)
73 | {
74 | debugText.text += "\n";
75 | debugText.text += "" + text;
76 | }
77 |
78 | /**
79 | * Returns an array of song names to use for music list
80 | * @param listPath file path to the txt file
81 | * @return An array of song names from the txt file
82 | */
83 | function fillMusicList(listPath:String):Array
84 | {
85 | return Assets.getText(listPath).split("\n").map(str -> str.trim());
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/example/source/Visualizer.hx:
--------------------------------------------------------------------------------
1 | package;
2 |
3 | import flixel.FlxG;
4 | import flixel.FlxSprite;
5 | import flixel.group.FlxGroup;
6 | import flixel.group.FlxGroup.FlxTypedGroup;
7 | import flixel.util.FlxColor;
8 | import funkin.vis.dsp.SpectralAnalyzer;
9 | import lime.media.AudioSource;
10 |
11 | class Visualizer extends FlxGroup
12 | {
13 | var grpBars:FlxTypedGroup;
14 | var peakLines:FlxTypedGroup;
15 | var analyzer:SpectralAnalyzer;
16 | var debugMode:Bool = false;
17 |
18 | public function new(audioSource:AudioSource, barCount:Int = 16)
19 | {
20 | super();
21 |
22 | analyzer = new SpectralAnalyzer(audioSource, barCount, 0.1, 10);
23 |
24 | grpBars = new FlxTypedGroup();
25 | add(grpBars);
26 | peakLines = new FlxTypedGroup();
27 | add(peakLines);
28 |
29 | for (i in 0...barCount)
30 | {
31 | var spr = new FlxSprite((i / barCount) * FlxG.width, 0).makeGraphic(Std.int((1 / barCount) * FlxG.width) - 4, FlxG.height, 0x55ff0000);
32 | spr.origin.set(0, FlxG.height);
33 | grpBars.add(spr);
34 | spr = new FlxSprite((i / barCount) * FlxG.width, 0).makeGraphic(Std.int((1 / barCount) * FlxG.width) - 4, 1, 0xaaff0000);
35 | peakLines.add(spr);
36 | }
37 | }
38 |
39 | @:generic
40 | static inline function min(x:T, y:T):T
41 | {
42 | return x > y ? y : x;
43 | }
44 |
45 | override function draw()
46 | {
47 | var levels = analyzer.getLevels();
48 |
49 | for (i in 0...min(grpBars.members.length, levels.length)) {
50 | grpBars.members[i].scale.y = levels[i].value;
51 | peakLines.members[i].y = FlxG.height - (levels[i].peak * FlxG.height);
52 | }
53 |
54 | if (debugMode) {
55 | lime.system.System.exit(0);
56 | }
57 | super.draw();
58 | }
59 |
60 | override public function update(elapsed:Float):Void
61 | {
62 | if (FlxG.keys.justReleased.ENTER)
63 | {
64 | debugMode = true;
65 | // The up arrow key is currently pressed
66 | // This code is executed every frame, while the key is pressed
67 | }
68 |
69 | super.update(elapsed);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/haxelib.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "funkin.vis",
3 | "url": "https://github.com/FunkinCrew/funkVis",
4 | "license": "MIT",
5 | "description": "Haxe FFT visulization stuff",
6 | "classPath": "src",
7 | "tags": ["audio", "fft", "visualization"],
8 | "releasenote": "Initial release",
9 | "version": "0.0.1",
10 | "dependencies": {
11 |
12 | }
13 | }
--------------------------------------------------------------------------------
/hmm.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": [
3 | {
4 | "name": "flixel",
5 | "type": "haxelib",
6 | "version": "5.3.1"
7 | },
8 | {
9 | "name": "funkin.vis",
10 | "type": "haxelib",
11 | "version": null
12 | },
13 | {
14 | "name": "grig.audio",
15 | "type": "git",
16 | "dir": "src",
17 | "ref": "cbf91e2180fd2e374924fe74844086aab7891666",
18 | "url": "https://gitlab.com/haxe-grig/grig.audio.git"
19 | },
20 | {
21 | "name": "hxcpp",
22 | "type": "haxelib",
23 | "version": null
24 | },
25 | {
26 | "name": "hxcpp-debug-server",
27 | "type": "haxelib",
28 | "version": "1.2.4"
29 | },
30 | {
31 | "name": "instrument",
32 | "type": "haxelib",
33 | "version": null
34 | },
35 | {
36 | "name": "lime",
37 | "type": "haxelib",
38 | "version": "8.0.2"
39 | },
40 | {
41 | "name": "openfl",
42 | "type": "haxelib",
43 | "version": "9.2.2"
44 | }
45 | ]
46 | }
--------------------------------------------------------------------------------
/src/funkin/vis/AudioBuffer.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis;
2 |
3 | import lime.utils.UInt16Array;
4 |
5 | class AudioBuffer
6 | {
7 | public var data(default, null):UInt16Array;
8 | public var sampleRate(default, null):Float;
9 |
10 | public function new(data:UInt16Array, sampleRate:Float)
11 | {
12 | this.data = data;
13 | this.sampleRate = sampleRate;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/funkin/vis/AudioClip.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis;
2 |
3 | /**
4 | * Represents a currently playing audio clip
5 | */
6 | interface AudioClip
7 | {
8 | public var audioBuffer(default, null):AudioBuffer;
9 | public var currentFrame(get, never):Int;
10 | public var source:Dynamic;
11 | }
--------------------------------------------------------------------------------
/src/funkin/vis/LogHelper.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis;
2 |
3 | class LogHelper
4 | {
5 | public static function log2(x:Float):Float
6 | {
7 | return Math.log(x) / Math.log(2);
8 | }
9 |
10 |
11 | public static function log10(x:Float):Float
12 | {
13 | return Math.log(x) / Math.log(10);
14 | };
15 | }
--------------------------------------------------------------------------------
/src/funkin/vis/Scaling.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis;
2 |
3 | import funkin.vis.LogHelper;
4 |
5 | class Scaling
6 | {
7 | public static inline function freqScaleMel(freq:Float):Float
8 | return LogHelper.log2(1 + freq / 700);
9 |
10 | public static inline function invFreqScaleMel(x:Float):Float
11 | return 700 * (Math.pow(2, x - 1));
12 |
13 | public static inline function freqScaleBark(freq:Float):Float
14 | return (26.81 * freq) / (1960 + freq) - 0.53;
15 |
16 | public static inline function invFreqScaleBark(x:Float):Float
17 | return 1960 / (26.81 / (x + .53) - 1);
18 |
19 | public static inline function freqScaleLog(freq:Float):Float
20 | return LogHelper.log10(1 + freq / 1000);
21 |
22 | public static inline function invFreqScaleLog(x:Float):Float
23 | return 1000 * (Math.pow(10, x - 1));
24 | }
--------------------------------------------------------------------------------
/src/funkin/vis/_internal/html5/AnalyzerNode.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis._internal.html5;
2 |
3 | import funkin.vis.AudioBuffer;
4 | #if lime_howlerjs
5 | import lime.media.howlerjs.Howl;
6 | import js.html.audio.AnalyserNode as AnalyseWebAudio;
7 | #end
8 |
9 | // note: analyze and analyse are both correct spellings of the word,
10 | // but "AnalyserNode" is the correct class name in the Web Audio API
11 | // and we use the Z variant here...
12 | class AnalyzerNode
13 | {
14 |
15 | #if lime_howlerjs
16 | public var analyzer:AnalyseWebAudio;
17 | public var maxDecibels:Float = -30;
18 | public var minDecibels:Float = -100;
19 | public var fftSize:Int = 2048;
20 | #end
21 |
22 | // #region yoooo
23 | public function new(?audioClip:AudioClip)
24 | {
25 | trace("Loading audioClip");
26 |
27 | #if lime_howlerjs
28 | analyzer = new AnalyseWebAudio(audioClip.source._sounds[0]._node.context);
29 | audioClip.source._sounds[0]._node.connect(analyzer);
30 | // trace(audioClip.source._sounds[0]._node.context.sampleRate);
31 | // trace(analyzer);
32 | // trace(analyzer.fftSize);
33 | // howler = cast buffer.source;
34 | // trace(howler);
35 | getFloatFrequencyData();
36 | #end
37 | }
38 |
39 | public function getFloatFrequencyData():Array
40 | {
41 | #if lime_howlerjs
42 | var array:js.lib.Float32Array = new js.lib.Float32Array(analyzer.frequencyBinCount);
43 | analyzer.fftSize = fftSize;
44 | analyzer.minDecibels = minDecibels;
45 | analyzer.maxDecibels = maxDecibels;
46 | analyzer.getFloatFrequencyData(array);
47 | return cast array;
48 | #end
49 | return [];
50 | }
51 | }
--------------------------------------------------------------------------------
/src/funkin/vis/audioclip/frontends/LimeAudioClip.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.audioclip.frontends;
2 |
3 | import flixel.FlxG;
4 | import flixel.math.FlxMath;
5 | import funkin.vis.AudioBuffer;
6 | import lime.media.AudioSource;
7 |
8 | /**
9 | * Implementation of AudioClip for Lime.
10 | * On OpenFL you will want SoundChannel.__source (with @:privateAccess)
11 | * For Flixel, you will want to get the FlxSound._channel.__source
12 | *
13 | * Note: On one of the recent OpenFL versions (9.3.2)
14 | * __source was renamed to __audioSource
15 | * https://github.com/openfl/openfl/commit/eec48a
16 | *
17 | */
18 | class LimeAudioClip implements funkin.vis.AudioClip
19 | {
20 | public var audioBuffer(default, null):AudioBuffer;
21 | public var currentFrame(get, never):Int;
22 | public var source:Dynamic;
23 |
24 | public function new(audioSource:AudioSource)
25 | {
26 | var data:lime.utils.UInt16Array = cast audioSource.buffer.data;
27 |
28 | #if web
29 | var sampleRate:Float = audioSource.buffer.src._sounds[0]._node.context.sampleRate;
30 | #else
31 | var sampleRate = audioSource.buffer.sampleRate;
32 | #end
33 |
34 | this.audioBuffer = new AudioBuffer(data, sampleRate);
35 | this.source = audioSource.buffer.src;
36 | }
37 |
38 | private function get_currentFrame():Int
39 | {
40 | var dataLength:Int = 0;
41 |
42 | #if web
43 | dataLength = source.length;
44 | #else
45 | dataLength = audioBuffer.data.length;
46 | #end
47 |
48 | var value = Std.int(FlxMath.remapToRange(FlxG.sound.music.time, 0, FlxG.sound.music.length, 0, dataLength));
49 |
50 | if (value < 0)
51 | return -1;
52 |
53 | return value;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/Complex.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.dsp;
2 |
3 | /**
4 | Complex number representation.
5 | **/
6 | @:forward(real, imag) @:notNull @:pure
7 | abstract Complex({
8 | final real : Float;
9 | final imag : Float;
10 | }) {
11 | public inline function new(real:Float, imag: Float)
12 | this = { real: real, imag: imag };
13 |
14 | /**
15 | Makes a Complex number with the given Float as its real part and a zero imag part.
16 | **/
17 | @:from
18 | public static inline function fromReal(r:Float)
19 | return new Complex(r, 0);
20 |
21 | /**
22 | Complex argument, in radians.
23 | **/
24 | public var angle(get,never) : Float;
25 | inline function get_angle()
26 | return Math.atan2(this.imag, this.real);
27 |
28 | /**
29 | Complex module.
30 | **/
31 | public var magnitude(get,never) : Float;
32 | inline function get_magnitude()
33 | return Math.sqrt(this.real*this.real + this.imag*this.imag);
34 |
35 | @:op(A + B)
36 | public inline function add(rhs:Complex) : Complex
37 | return new Complex(this.real + rhs.real, this.imag + rhs.imag);
38 |
39 | @:op(A - B)
40 | public inline function sub(rhs:Complex) : Complex
41 | return new Complex(this.real - rhs.real, this.imag - rhs.imag);
42 |
43 | @:op(A * B)
44 | public inline function mult(rhs:Complex) : Complex
45 | return new Complex(this.real*rhs.real - this.imag*rhs.imag,
46 | this.real*rhs.imag + this.imag*rhs.real);
47 |
48 | /**
49 | Returns the complex conjugate, does not modify this object.
50 | **/
51 | public inline function conj() : Complex
52 | return new Complex(this.real, -this.imag);
53 |
54 | /**
55 | Multiplication by a real factor, does not modify this object.
56 | **/
57 | public inline function scale(k:Float) : Complex
58 | return new Complex(this.real * k, this.imag * k);
59 |
60 | public inline function copy() : Complex
61 | return new Complex(this.real, this.imag);
62 |
63 | /**
64 | The imaginary unit.
65 | **/
66 | public static final im = new Complex(0, 1);
67 |
68 | /**
69 | The complex zero.
70 | **/
71 | public static final zero = new Complex(0, 0);
72 |
73 | /**
74 | Computes the complex exponential `e^(iw)`.
75 | **/
76 | public static inline function exp(w:Float)
77 | return new Complex(Math.cos(w), Math.sin(w));
78 | }
79 |
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/FFT.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.dsp;
2 |
3 | import funkin.vis.dsp.Complex;
4 | import haxe.ds.Vector;
5 |
6 | // these are only used for testing, down in FFT.main()
7 | using funkin.vis.dsp.OffsetArray;
8 | using funkin.vis.dsp.Signal;
9 |
10 |
11 | /**
12 | Fast/Finite Fourier Transforms.
13 | **/
14 | class FFT {
15 | /**
16 | Computes the Discrete Fourier Transform (DFT) of a `Complex` sequence.
17 |
18 | If the input has N data points (N should be a power of 2 or padding will be added)
19 | from a signal sampled at intervals of 1/Fs, the result will be a sequence of N
20 | samples from the Discrete-Time Fourier Transform (DTFT) - which is Fs-periodic -
21 | with a spacing of Fs/N Hz between them and a scaling factor of Fs.
22 | **/
23 | public static function fft(input:Array) : Array
24 | return do_fft(input, false);
25 |
26 | /**
27 | Like `fft`, but for a real (Float) sequence input.
28 |
29 | Since the input time signal is real, its frequency representation is
30 | Hermitian-symmetric so we only return the positive frequencies.
31 | **/
32 | public static function rfft(input:Array) : Array {
33 | // checkAndComputeTwiddles(input.length);
34 | final s = fft(input.map(Complex.fromReal));
35 | return s.slice(0, Std.int(s.length / 2) + 1);
36 | }
37 |
38 | /**
39 | Computes the Inverse DFT of a periodic input sequence.
40 |
41 | If the input contains N (a power of 2) DTFT samples, each spaced Fs/N Hz
42 | from each other, the result will consist of N data points as sampled
43 | from a time signal at intervals of 1/Fs with a scaling factor of 1/Fs.
44 | **/
45 | public static function ifft(input:Array) : Array
46 | return do_fft(input, true);
47 |
48 | // Handles padding and scaling for forwards and inverse FFTs.
49 | private static function do_fft(input:Array, inverse:Bool) : Array {
50 | final n = nextPow2(input.length);
51 | var ts = [for (i in 0...n) if (i < input.length) input[i] else Complex.zero];
52 | var fs = [for (_ in 0...n) Complex.zero];
53 |
54 | if (inverse && twiddleFactorsInversed?.length != n)
55 | precomputeTwiddleFactors(n, true);
56 | else if (!inverse && twiddleFactors?.length != n)
57 | precomputeTwiddleFactors(n, false);
58 |
59 | ditfft4(ts, 0, fs, 0, n, 1, inverse);
60 | return inverse ? fs.map(z -> z.scale(1 / n)) : fs;
61 | }
62 |
63 |
64 | // Radix-2 Decimation-In-Time variant of Cooley–Tukey's FFT, recursive.
65 | private static function ditfft2(
66 | time:Array, t:Int,
67 | freq:Array, f:Int,
68 | n:Int, step:Int, inverse: Bool
69 | ) : Void {
70 | if (n == 1) {
71 | freq[f] = time[t].copy();
72 | } else {
73 | final halfLen = Std.int(n / 2);
74 | ditfft2(time, t, freq, f, halfLen, step * 2, inverse);
75 | ditfft2(time, t + step, freq, f + halfLen, halfLen, step * 2, inverse);
76 | for (k in 0...halfLen) {
77 | final twiddle = inverse ? twiddleFactorsInversed[k] : twiddleFactors[k];
78 | final even = freq[f + k].copy();
79 | final odd = freq[f + k + halfLen].copy();
80 | freq[f + k] = even + twiddle * odd;
81 | freq[f + k + halfLen] = even - twiddle * odd;
82 | }
83 | }
84 | }
85 |
86 | private static function ditfft4(time:Array, t:Int, freq:Array, f:Int, n:Int, step:Int, inverse:Bool):Void {
87 |
88 | if (n == 4) {
89 | // Base case: Compute the 4-point DFT directly
90 | for (k in 0...n) {
91 | var sum = Complex.zero;
92 | for (j in 0...4) {
93 | var twiddle = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k / n);
94 | sum += time[t + j * step] * twiddle;
95 | }
96 | freq[f + k] = sum;
97 | }
98 | } else {
99 | final quarterLen = Std.int(n / 4);
100 | ditfft4(time, t, freq, f, quarterLen, step * 4, inverse);
101 | ditfft4(time, t + step, freq, f + quarterLen, quarterLen, step * 4, inverse);
102 | ditfft4(time, t + 2 * step, freq, f + 2 * quarterLen, quarterLen, step * 4, inverse);
103 | ditfft4(time, t + 3 * step, freq, f + 3 * quarterLen, quarterLen, step * 4, inverse);
104 |
105 | for (k in 0...quarterLen) {
106 | final twiddle0 = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k / n);
107 | final twiddle1 = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k / n);
108 | final twiddle2 = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k * 2 / n);
109 | final twiddle3 = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k * 3 / n);
110 |
111 | final f0 = freq[f + k].copy();
112 | final f1 = freq[f + k + quarterLen].copy() * twiddle1;
113 | final f2 = freq[f + k + 2 * quarterLen].copy() * twiddle2;
114 | final f3 = freq[f + k + 3 * quarterLen].copy() * twiddle3;
115 |
116 | freq[f + k] = f0 + f1 + f2 + f3;
117 | freq[f + k + quarterLen] = f0 + f1 - f2 - f3;
118 | freq[f + k + 2 * quarterLen] = f0 - f1 - f2 + f3;
119 | freq[f + k + 3 * quarterLen] = f0 - f1 + f2 - f3;
120 | }
121 | }
122 | }
123 |
124 | // Naive O(n^2) DFT, used for testing purposes.
125 | private static function dft(ts:Array, ?inverse:Bool) : Array {
126 | if (inverse == null) inverse = false;
127 | final n = ts.length;
128 | var fs = new Array();
129 | fs.resize(n);
130 | for (f in 0...n) {
131 | var sum = Complex.zero;
132 | for (t in 0...n) {
133 | sum += ts[t] * Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * f * t / n);
134 | }
135 | fs[f] = inverse ? sum.scale(1 / n) : sum;
136 | }
137 | return fs;
138 | }
139 |
140 | private static var twiddleFactorsInversed:Array;
141 |
142 | private static var twiddleFactors:Array;
143 |
144 | private static function precomputeTwiddleFactors(maxN:Int, inverse:Bool):Void
145 | {
146 | var n:Int = maxN;
147 | var base_len = maxN;
148 | var len = base_len * (1 << 2);
149 | var twiddles:Array = [];
150 | // for (k in 0...Std.int(n / 2)) { // n/2 because of symmetry
151 | // var twiddle:Complex = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k / n);
152 | // twiddles.push(twiddle);
153 | // }
154 |
155 |
156 | // radix2 twiddles
157 | for (k in 0...Std.int(n / 2)) { // n/4 because of symmetry in Radix-4
158 | var twiddle:Complex = computeTwiddle(k, n, inverse);
159 | twiddles.push(twiddle);
160 | }
161 |
162 | if (inverse)
163 | twiddleFactorsInversed = twiddles;
164 | else
165 | twiddleFactors = twiddles;
166 | }
167 |
168 | private static function computeTwiddle(index, fft_len, inverse:Bool = false)
169 | {
170 | var constant = -2 * Math.PI / fft_len;
171 | var angle = constant * index;
172 |
173 | var result:Complex = new Complex(Math.cos(angle), Math.sin(angle));
174 |
175 | if (inverse)
176 | return result.conj();
177 | else
178 | return result;
179 | }
180 |
181 | private static function useTwiddleFactor(n:Int, k:Int, inverse:Bool = false):Complex {
182 | // Compute the index adjustment based on the FFT size n
183 | // var indexAdjustment:Int = Std.int(twiddleFactors.length / (n / 4));
184 | var twiddlesToUse = inverse ? twiddleFactorsInversed : twiddleFactors;
185 | return twiddlesToUse[k];
186 | }
187 |
188 | /**
189 | Finds the power of 2 that is equal to or greater than the given natural.
190 | **/
191 | public static function nextPow2(x:Int) : Int {
192 | if (x < 2) return 1;
193 | else if ((x & (x-1)) == 0) return x;
194 | var pow = 2;
195 | x--;
196 | while ((x >>= 1) != 0) pow <<= 1;
197 | return pow;
198 | }
199 |
200 | // testing, but also acts like an example
201 | static function main() {
202 | // sampling and buffer parameters
203 | final Fs = 44100.0;
204 | final N = 512;
205 | final halfN = Std.int(N / 2);
206 |
207 | // build a time signal as a sum of sinusoids
208 | final freqs = [5919.911];
209 | final ts = [for (n in 0...N) freqs.map(f -> Math.sin(2 * Math.PI * f * n / Fs)).sum()];
210 |
211 | // get positive spectrum and use its symmetry to reconstruct negative domain
212 | final fs_pos = rfft(ts);
213 | final fs_fft = new OffsetArray(
214 | [for (k in -(halfN - 1) ... 0) fs_pos[-k].conj()].concat(fs_pos),
215 | -(halfN - 1)
216 | );
217 |
218 | // double-check with naive DFT
219 | final fs_dft = new OffsetArray(
220 | dft(ts.map(Complex.fromReal)).circShift(halfN - 1),
221 | -(halfN - 1)
222 | );
223 | final fs_err = [for (k in -(halfN - 1) ... halfN) fs_fft[k] - fs_dft[k]];
224 | final max_fs_err = fs_err.map(z -> z.magnitude).max();
225 | if (max_fs_err > 1e-6) haxe.Log.trace('FT Error: ${max_fs_err}', null);
226 |
227 | // find spectral peaks to detect signal frequencies
228 | final freqis = fs_fft.array.map(z -> z.magnitude)
229 | .findPeaks()
230 | .map(k -> (k - (halfN - 1)) * Fs / N)
231 | .filter(f -> f >= 0);
232 | if (freqis.length != freqs.length) {
233 | trace('Found frequencies: ${freqis}');
234 | } else {
235 | final freqs_err = [for (i in 0...freqs.length) freqis[i] - freqs[i]];
236 | final max_freqs_err = freqs_err.map(Math.abs).max();
237 | if (max_freqs_err > Fs / N) trace('Frequency Errors: ${freqs_err}');
238 | }
239 |
240 | // recover time signal from the frequency domain
241 | final ts_ifft = ifft(fs_fft.array.circShift(-(halfN - 1)).map(z -> z.scale(1 / Fs)));
242 | final ts_err = [for (n in 0...N) ts_ifft[n].scale(Fs).real - ts[n]];
243 | final max_ts_err = ts_err.map(Math.abs).max();
244 | if (max_ts_err > 1e-6) haxe.Log.trace('IFT Error: ${max_ts_err}', null);
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/OffsetArray.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.dsp;
2 |
3 | /**
4 | A view into an Array with an indexing offset.
5 |
6 | Usages include 1-indexed sequences or zero-centered buffers with negative indexing.
7 | **/
8 | @:forward(array, offset)
9 | abstract OffsetArray({
10 | final array : Array;
11 | final offset : Int;
12 | }) {
13 | public inline function new(array:Array, offset:Int)
14 | this = { array: array, offset: offset };
15 |
16 | public var length(get,never) : Int;
17 | inline function get_length()
18 | return this.array.length;
19 |
20 | @:arrayAccess
21 | public inline function get(index:Int) : T
22 | return this.array[index - this.offset];
23 |
24 | @:arrayAccess
25 | public inline function set(index:Int, value:T) : Void
26 | this.array[index - this.offset] = value;
27 |
28 | /**
29 | Iterates through items in their original order while providing the altered indexes as keys.
30 | **/
31 | public inline function keyValueIterator() : KeyValueIterator
32 | return new OffsetArrayIterator(this.array, this.offset);
33 |
34 | @:from
35 | static inline function fromArray(array:Array)
36 | return new OffsetArray(array, 0);
37 |
38 | @:to
39 | inline function toArray()
40 | return this.array;
41 |
42 | /**
43 | Makes a shifted version of the given `array`, where elements are in the
44 | same order but shifted by `n` positions (to the right if positive and to
45 | the left if negative) in **circular** fashion (no elements discarded).
46 | **/
47 | public static function circShift(array:Array, n:Int) : Array {
48 | if (n < 0) return circShift(array, array.length + n);
49 |
50 | var shifted = new Array();
51 |
52 | n = n % array.length;
53 | for (i in array.length - n ... array.length) shifted.push(array[i]);
54 | for (i in 0 ... array.length - n) shifted.push(array[i]);
55 |
56 | return shifted;
57 | }
58 | }
59 |
60 | private class OffsetArrayIterator {
61 | private final array : Array;
62 | private final offset : Int;
63 | private var enumeration : Int;
64 |
65 | public inline function new(array:Array, offset:Int) {
66 | this.array = array;
67 | this.offset = offset;
68 | this.enumeration = 0;
69 | }
70 |
71 | public inline function next() : {key:Int, value:T} {
72 | final i = this.enumeration++;
73 | return { key: i + this.offset, value: this.array[i] };
74 | }
75 |
76 | public inline function hasNext() : Bool
77 | return this.enumeration < this.array.length;
78 | }
79 |
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/README.md:
--------------------------------------------------------------------------------
1 | haxedsp (this folder) is provided via https://github.com/baioc/hxdsp/tree/master, under the Unlicense license!!
2 |
3 | It's not in a haxelib, so I'm including it all in here for nice convienience!
4 |
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/RecentPeakFinder.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.dsp;
2 |
3 | class RecentPeakFinder
4 | {
5 | private var buffer:Array;
6 | private var bufferIndex:Int = 0; // We circle arround to avoid reallocating
7 | public var peak(default, null):Float = 0;
8 | public var lastValue(get, never):Float;
9 |
10 | public function new(length:Int = 30) {
11 | buffer = new Array();
12 | buffer.resize(length);
13 | }
14 |
15 | public function push(value:Float) {
16 | buffer[bufferIndex] = value;
17 | if (value > peak) peak = value;
18 | else peak = Signal.max(buffer);
19 | bufferIndex = if (bufferIndex + 1 == buffer.length) 0;
20 | else bufferIndex + 1;
21 | }
22 |
23 | private function get_lastValue():Float {
24 | return if (bufferIndex == 0) buffer[buffer.length - 1];
25 | else buffer[bufferIndex - 1];
26 | }
27 | }
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/Signal.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.dsp;
2 |
3 | using Lambda;
4 |
5 |
6 | /**
7 | Signal processing miscellaneous utilities.
8 | **/
9 | class Signal {
10 | /**
11 | Returns a smoothed version of the input array using a moving average.
12 | **/
13 | public static function smooth(y:Array, n:Int) : Array {
14 | if (n <= 0) {
15 | return null;
16 | } else if (n == 1) {
17 | return y.copy();
18 | } else {
19 | var smoothed = new Array();
20 | smoothed.resize(y.length);
21 | for (i in 0...y.length) {
22 | var m = i + 1 < n ? i : n - 1;
23 | smoothed[i] = sum(y.slice(i - m, i + 1));
24 | }
25 | return smoothed;
26 | }
27 | }
28 |
29 | /**
30 | Finds indexes of peaks in the order they appear in the input sequence.
31 |
32 | @param threshold Minimal peak height wrt. its neighbours, defaults to 0.
33 | @param minHeight Minimal peak height wrt. the whole input, defaults to global minimum.
34 | **/
35 | public static function findPeaks(
36 | y:Array,
37 | ?threshold:Float,
38 | ?minHeight:Float
39 | ) : Array {
40 | threshold = threshold == null ? 0.0 : Math.abs(threshold);
41 | minHeight = minHeight == null ? Signal.min(y) : minHeight;
42 |
43 | var peaks = new Array();
44 |
45 | final dy = [for (i in 1...y.length) y[i] - y[i-1]];
46 | for (i in 1...dy.length) {
47 | // peak: function growth positive to its left and negative to its right
48 | if (
49 | dy[i-1] > threshold && dy[i] < -threshold &&
50 | y[i] > minHeight
51 | ) {
52 | peaks.push(i);
53 | }
54 | }
55 |
56 | return peaks;
57 | }
58 |
59 | /**
60 | Returns the sum of all the elements of a given array.
61 |
62 | This function tries to minimize floating-point precision errors.
63 | **/
64 | public static function sum(array:Array) : Float {
65 | // Neumaier's "improved Kahan-Babuska algorithm":
66 |
67 | var sum = 0.0;
68 | var c = 0.0; // running compensation for lost precision
69 |
70 | for (v in array) {
71 | var t = sum + v;
72 | c += Math.abs(sum) >= Math.abs(v)
73 | ? (sum - t) + v // sum is bigger => low-order digits of v are lost
74 | : (v - t) + sum; // v is bigger => low-order digits of sum are lost
75 | sum = t;
76 | }
77 |
78 | return sum + c; // correction only applied at the very end
79 | }
80 |
81 | /**
82 | Returns the average value of an array.
83 | **/
84 | public static function mean(y:Array) : Float
85 | return sum(y) / y.length;
86 |
87 | /**
88 | Returns the global maximum.
89 | **/
90 | public static function max(y:Array) : Float
91 | return y.fold(Math.max, y[0]);
92 |
93 | /**
94 | Returns the global maximum's index.
95 | **/
96 | public static function maxi(y:Array) : Int
97 | return y.foldi((yi, m, i) -> yi > y[m] ? i : m, 0);
98 |
99 | /**
100 | Returns the global minimum.
101 | **/
102 | public static function min(y:Array) : Float
103 | return y.fold(Math.min, y[0]);
104 |
105 | /**
106 | Returns the global minimum's index.
107 | **/
108 | public static function mini(y:Array) : Int
109 | return y.foldi((yi, m, i) -> yi < y[m] ? i : m, 0);
110 | }
111 |
--------------------------------------------------------------------------------
/src/funkin/vis/dsp/SpectralAnalyzer.hx:
--------------------------------------------------------------------------------
1 | package funkin.vis.dsp;
2 |
3 | import flixel.FlxG;
4 | import flixel.math.FlxMath;
5 | import funkin.vis._internal.html5.AnalyzerNode;
6 | import funkin.vis.audioclip.frontends.LimeAudioClip;
7 | import grig.audio.FFT;
8 | import grig.audio.FFTVisualization;
9 | import lime.media.AudioSource;
10 |
11 | using grig.audio.lime.UInt8ArrayTools;
12 |
13 | typedef Bar =
14 | {
15 | var value:Float;
16 | var peak:Float;
17 | }
18 |
19 | typedef BarObject =
20 | {
21 | var binLo:Int;
22 | var binHi:Int;
23 | var freqLo:Float;
24 | var freqHi:Float;
25 | var recentValues:RecentPeakFinder;
26 | }
27 |
28 | enum MathType
29 | {
30 | Round;
31 | Floor;
32 | Ceil;
33 | Cast;
34 | }
35 |
36 | class SpectralAnalyzer
37 | {
38 | public var minDb(default, set):Float = -70;
39 | public var maxDb(default, set):Float = -20;
40 | public var fftN(default, set):Int = 4096;
41 | public var minFreq:Float = 50;
42 | public var maxFreq:Float = 22000;
43 | // Awkwardly, we'll have to interfaces for now because there's too much platform specific stuff we need
44 | private var audioSource:AudioSource;
45 | private var audioClip:AudioClip;
46 | private var barCount:Int;
47 | private var maxDelta:Float;
48 | private var peakHold:Int;
49 | var fftN2:Int = 2048;
50 | #if web
51 | private var htmlAnalyzer:AnalyzerNode;
52 | private var bars:Array = [];
53 | #else
54 | private var fft:FFT;
55 | private var vis = new FFTVisualization();
56 | private var barHistories = new Array();
57 | private var blackmanWindow = new Array();
58 | #end
59 |
60 | private function freqToBin(freq:Float, mathType:MathType = Round):Int
61 | {
62 | var bin = freq * fftN2 / audioClip.audioBuffer.sampleRate;
63 | return switch (mathType) {
64 | case Round: Math.round(bin);
65 | case Floor: Math.floor(bin);
66 | case Ceil: Math.ceil(bin);
67 | case Cast: Std.int(bin);
68 | }
69 | }
70 |
71 | function normalizedB(value:Float)
72 | {
73 | var maxValue = maxDb;
74 | var minValue = minDb;
75 |
76 | return clamp((value - minValue) / (maxValue - minValue), 0, 1);
77 | }
78 |
79 | function calcBars(barCount:Int, peakHold:Int)
80 | {
81 | #if web
82 | bars = [];
83 | var logStep = (LogHelper.log10(maxFreq) - LogHelper.log10(minFreq)) / (barCount);
84 |
85 | var scaleMin:Float = Scaling.freqScaleLog(minFreq);
86 | var scaleMax:Float = Scaling.freqScaleLog(maxFreq);
87 |
88 | var curScale:Float = scaleMin;
89 |
90 | // var stride = (scaleMax - scaleMin) / bands;
91 |
92 | for (i in 0...barCount)
93 | {
94 | var curFreq:Float = Math.pow(10, LogHelper.log10(minFreq) + (logStep * i));
95 |
96 | var freqLo:Float = curFreq;
97 | var freqHi:Float = Math.pow(10, LogHelper.log10(minFreq) + (logStep * (i + 1)));
98 |
99 | var binLo = freqToBin(freqLo, Floor);
100 | var binHi = freqToBin(freqHi);
101 |
102 | bars.push(
103 | {
104 | binLo: binLo,
105 | binHi: binHi,
106 | freqLo: freqLo,
107 | freqHi: freqHi,
108 | recentValues: new RecentPeakFinder(peakHold)
109 | });
110 | }
111 |
112 | if (bars[0].freqLo < minFreq)
113 | {
114 | bars[0].freqLo = minFreq;
115 | bars[0].binLo = freqToBin(minFreq, Floor);
116 | }
117 |
118 | if (bars[bars.length - 1].freqHi > maxFreq)
119 | {
120 | bars[bars.length - 1].freqHi = maxFreq;
121 | bars[bars.length - 1].binHi = freqToBin(maxFreq, Floor);
122 | }
123 | #else
124 | if (barCount > barHistories.length) {
125 | barHistories.resize(barCount);
126 | }
127 | for (i in 0...barCount) {
128 | if (barHistories[i] == null) barHistories[i] = new RecentPeakFinder();
129 | }
130 | #end
131 | }
132 |
133 | function resizeBlackmanWindow(size:Int)
134 | {
135 | #if !web
136 | if (blackmanWindow.length == size) return;
137 | blackmanWindow.resize(size);
138 | for (i in 0...size) {
139 | blackmanWindow[i] = calculateBlackmanWindow(i, size);
140 | }
141 | #end
142 | }
143 |
144 | public function new(audioSource:AudioSource, barCount:Int, maxDelta:Float = 0.01, peakHold:Int = 30)
145 | {
146 | this.audioSource = audioSource;
147 | this.audioClip = new LimeAudioClip(audioSource);
148 | this.barCount = barCount;
149 | this.maxDelta = maxDelta;
150 | this.peakHold = peakHold;
151 |
152 | #if web
153 | htmlAnalyzer = new AnalyzerNode(audioClip);
154 | #else
155 | fft = new FFT(fftN);
156 | #end
157 |
158 | calcBars(barCount, peakHold);
159 | resizeBlackmanWindow(fftN);
160 | }
161 |
162 | public function getLevels(?levels:Array):Array
163 | {
164 | if(levels == null) levels = new Array();
165 | #if web
166 | var amplitudes:Array = htmlAnalyzer.getFloatFrequencyData();
167 |
168 | for (i in 0...bars.length) {
169 | var bar = bars[i];
170 | var binLo = bar.binLo;
171 | var binHi = bar.binHi;
172 |
173 | var value:Float = minDb;
174 | for (j in (binLo + 1)...(binHi)) {
175 | value = Math.max(value, amplitudes[Std.int(j)]);
176 | }
177 |
178 | // this isn't for clamping, it's to get a value
179 | // between 0 and 1!
180 | value = normalizedB(value);
181 | bar.recentValues.push(value);
182 | var recentPeak = bar.recentValues.peak;
183 |
184 | if(levels[i] != null)
185 | {
186 | levels[i].value = value;
187 | levels[i].peak = recentPeak;
188 | }
189 | else levels.push({value: value, peak: recentPeak});
190 | }
191 |
192 | return levels;
193 | #else
194 | var numOctets = Std.int(audioSource.buffer.bitsPerSample / 8);
195 | var wantedLength = fftN * numOctets * audioSource.buffer.channels;
196 | var startFrame = audioClip.currentFrame;
197 |
198 | if (startFrame < 0)
199 | {
200 | return levels = [for (bar in 0...barCount) {value: 0, peak: 0}];
201 | }
202 |
203 | startFrame -= startFrame % numOctets;
204 | var segment = audioSource.buffer.data.subarray(startFrame, min(startFrame + wantedLength, audioSource.buffer.data.length));
205 |
206 | var signal = getSignal(segment, audioSource.buffer.bitsPerSample);
207 |
208 | if (audioSource.buffer.channels > 1) {
209 | var mixed = new Array();
210 | mixed.resize(Std.int(signal.length / audioSource.buffer.channels));
211 | for (i in 0...mixed.length) {
212 | mixed[i] = 0.0;
213 | for (c in 0...audioSource.buffer.channels) {
214 | mixed[i] += 0.7 * signal[i*audioSource.buffer.channels+c];
215 | }
216 | mixed[i] *= blackmanWindow[i];
217 | }
218 | signal = mixed;
219 | }
220 |
221 | var range = 16;
222 | var freqs = fft.calcFreq(signal);
223 | var bars = vis.makeLogGraph(freqs, barCount + 1, Math.floor(maxDb - minDb), range);
224 |
225 | if (bars.length - 1 > barHistories.length) {
226 | barHistories.resize(bars.length - 1);
227 | }
228 |
229 |
230 | levels.resize(bars.length-1);
231 | for (i in 0...bars.length-1) {
232 |
233 | if (barHistories[i] == null) barHistories[i] = new RecentPeakFinder();
234 | var recentValues = barHistories[i];
235 | var value = bars[i] / range;
236 |
237 | // slew limiting
238 | var lastValue = recentValues.lastValue;
239 | if (maxDelta > 0.0) {
240 | var delta = clamp(value - lastValue, -1 * maxDelta, maxDelta);
241 | value = lastValue + delta;
242 | }
243 | recentValues.push(value);
244 |
245 | var recentPeak = recentValues.peak;
246 |
247 | if(levels[i] != null)
248 | {
249 | levels[i].value = value;
250 | levels[i].peak = recentPeak;
251 | }
252 | else levels[i] = {value: value, peak: recentPeak};
253 | }
254 | return levels;
255 | #end
256 | }
257 |
258 | // Prevents a memory leak by reusing array
259 | var _buffer:Array = [];
260 | function getSignal(data:lime.utils.UInt8Array, bitsPerSample:Int):Array
261 | {
262 | switch(bitsPerSample)
263 | {
264 | case 8:
265 | _buffer.resize(data.length);
266 | for (i in 0...data.length)
267 | _buffer[i] = data[i] / 128.0;
268 |
269 | case 16:
270 | _buffer.resize(Std.int(data.length / 2));
271 | for (i in 0..._buffer.length)
272 | _buffer[i] = data.getInt16(i * 2) / 32767.0;
273 |
274 | case 24:
275 | _buffer.resize(Std.int(data.length / 3));
276 | for (i in 0..._buffer.length)
277 | _buffer[i] = data.getInt24(i * 3) / 8388607.0;
278 |
279 | case 32:
280 | _buffer.resize(Std.int(data.length / 4));
281 | for (i in 0..._buffer.length)
282 | _buffer[i] = data.getInt32(i * 4) / 2147483647.0;
283 |
284 | default: trace('Unknown integer audio format');
285 | }
286 | return _buffer;
287 | }
288 |
289 | @:generic
290 | static inline function clamp(val:T, min:T, max:T):T
291 | {
292 | return val <= min ? min : val >= max ? max : val;
293 | }
294 |
295 | static function calculateBlackmanWindow(n:Int, fftN:Int)
296 | {
297 | return 0.42 - 0.50 * Math.cos(2 * Math.PI * n / (fftN - 1)) + 0.08 * Math.cos(4 * Math.PI * n / (fftN - 1));
298 | }
299 |
300 | @:generic
301 | static public inline function min(x:T, y:T):T
302 | {
303 | return x > y ? y : x;
304 | }
305 |
306 | function set_minDb(value:Float):Float
307 | {
308 | minDb = value;
309 |
310 | #if web
311 | htmlAnalyzer.minDecibels = value;
312 | #end
313 |
314 | return value;
315 | }
316 |
317 | function set_maxDb(value:Float):Float
318 | {
319 | maxDb = value;
320 |
321 | #if web
322 | htmlAnalyzer.maxDecibels = value;
323 | #end
324 |
325 | return value;
326 | }
327 |
328 | function set_fftN(value:Int):Int
329 | {
330 | fftN = value;
331 | var pow2 = FFT.nextPow2(value);
332 | fftN2 = Std.int(pow2 / 2);
333 |
334 | #if web
335 | htmlAnalyzer.fftSize = pow2;
336 | #else
337 | fft = new FFT(value);
338 | #end
339 |
340 | calcBars(barCount, peakHold);
341 | resizeBlackmanWindow(fftN);
342 | return pow2;
343 | }
344 | }
345 |
--------------------------------------------------------------------------------