├── .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 | --------------------------------------------------------------------------------