From e8fa7f9c70be65638f876ba5b566b1d85a2f7555 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 23 Jan 2024 22:47:27 -0500 Subject: [PATCH] Performant audio waveforms generated directly from provided FlxSound elements. --- source/funkin/audio/waveform/WaveformData.hx | 95 +++++-- .../audio/waveform/WaveformDataParser.hx | 108 ++++++++ .../funkin/audio/waveform/WaveformSprite.hx | 244 ++++++++++++++++++ source/funkin/ui/debug/WaveformTestState.hx | 171 ++++-------- 4 files changed, 474 insertions(+), 144 deletions(-) create mode 100644 source/funkin/audio/waveform/WaveformSprite.hx diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx index 525d1bd5f..11d3ff641 100644 --- a/source/funkin/audio/waveform/WaveformData.hx +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -5,6 +5,8 @@ import funkin.util.MathUtil; @:nullSafety class WaveformData { + static final DEFAULT_VERSION:Int = 2; + /** * The version of the waveform data format. * @default `2` (-1 if not specified/invalid) @@ -25,7 +27,7 @@ class WaveformData * Lower values can more accurately represent the waveform when zoomed in, but take more data. */ @:alias('samples_per_pixel') - public var samplesPerPixel(default, null):Int = 256; + public var samplesPerPoint(default, null):Int = 256; /** * Number of bits to use for each sample value. Valid values are `8` and `16`. @@ -33,9 +35,9 @@ class WaveformData public var bits(default, null):Int = 16; /** - * Number of output waveform data points. + * The length of the data array, in points. */ - public var length(default, null):Int = 0; // Array size is (4 * length) + public var length(default, null):Int = 0; /** * Array of Int16 values representing the waveform. @@ -46,7 +48,16 @@ class WaveformData @:jignored var channelData:Null> = null; - public function new() {} + public function new(?version:Int, channels:Int, sampleRate:Int, samplesPerPoint:Int, bits:Int, length:Int, data:Array) + { + this.version = version ?? DEFAULT_VERSION; + this.channels = channels; + this.sampleRate = sampleRate; + this.samplesPerPoint = samplesPerPoint; + this.bits = bits; + this.length = length; + this.data = data; + } function buildChannelData():Array { @@ -79,7 +90,7 @@ class WaveformData */ public function maxSampleValue():Int { - if (_maxSampleValue != -1) return _maxSampleValue; + if (_maxSampleValue != 0) return _maxSampleValue; return _maxSampleValue = Std.int(Math.pow(2, bits)); } @@ -87,14 +98,14 @@ class WaveformData * Cache the value because `Math.pow` is expensive and the value gets used a lot. */ @:jignored - var _maxSampleValue:Int = -1; + var _maxSampleValue:Int = 0; /** * @return The length of the waveform in samples. */ public function lenSamples():Int { - return length * samplesPerPixel; + return length * samplesPerPoint; } /** @@ -110,7 +121,7 @@ class WaveformData */ public function secondsToIndex(seconds:Float):Int { - return Std.int(seconds * sampleRate / samplesPerPixel); + return Std.int(seconds * pointsPerSecond()); } /** @@ -118,7 +129,15 @@ class WaveformData */ public function indexToSeconds(index:Int):Float { - return index * samplesPerPixel / sampleRate; + return index / pointsPerSecond(); + } + + /** + * The number of data points this waveform data provides per second of audio. + */ + public inline function pointsPerSecond():Float + { + return sampleRate / samplesPerPoint; } /** @@ -136,8 +155,50 @@ class WaveformData { return index / length; } + + /** + * Resample the waveform data to create a new WaveformData object matching the desired `samplesPerPoint` value. + * This is useful for zooming in/out of the waveform in a performant manner. + * + * @param newSamplesPerPoint The new value for `samplesPerPoint`. + */ + public function resample(newSamplesPerPoint:Int):WaveformData + { + var result = this.clone(); + + var ratio = newSamplesPerPoint / samplesPerPoint; + if (ratio == 1) return result; + if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.'); + + var inputSampleCount = this.lenSamples(); + var outputSampleCount = Std.int(inputSampleCount * ratio); + + var inputPointCount = this.length; + var outputPointCount = Std.int(inputPointCount / ratio); + var outputChannelCount = this.channels; + + // TODO: Actually figure out the dumbass logic for this. + + return result; + } + + /** + * Create a new WaveformData whose parameters match the current object. + */ + public function clone(?newData:Array = null):WaveformData + { + if (newData == null) + { + newData = this.data.clone(); + } + + var clone = new WaveformData(this.version, this.channels, this.sampleRate, this.samplesPerPoint, this.bits, newData.length, newData); + + return clone; + } } +@:nullSafety class WaveformDataChannel { var parent:WaveformData; @@ -149,6 +210,9 @@ class WaveformDataChannel this.channelId = channelId; } + /** + * Retrieve a given minimum point at an index. + */ public function minSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2; @@ -165,7 +229,7 @@ class WaveformDataChannel /** * Minimum value within the range of samples. - * @param i + * NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead. */ public function minSampleRange(start:Int, end:Int) { @@ -180,13 +244,15 @@ class WaveformDataChannel /** * Maximum value within the range of samples, mapped to a value between 0 and 1. - * @param i */ public function minSampleRangeMapped(start:Int, end:Int) { return minSampleRange(start, end) / parent.maxSampleValue(); } + /** + * Retrieve a given maximum point at an index. + */ public function maxSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2 + 1; @@ -203,7 +269,7 @@ class WaveformDataChannel /** * Maximum value within the range of samples. - * @param i + * NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead. */ public function maxSampleRange(start:Int, end:Int) { @@ -218,17 +284,12 @@ class WaveformDataChannel /** * Maximum value within the range of samples, mapped to a value between 0 and 1. - * @param i */ public function maxSampleRangeMapped(start:Int, end:Int) { return maxSampleRange(start, end) / parent.maxSampleValue(); } - /** - * Maximum value within the range of samples, mapped to a value between 0 and 1. - * @param i - */ public function setMinSample(i:Int, value:Int) { var offset = (i * parent.channels + this.channelId) * 2; diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx index fb453cf1b..2e5c52d13 100644 --- a/source/funkin/audio/waveform/WaveformDataParser.hx +++ b/source/funkin/audio/waveform/WaveformDataParser.hx @@ -2,6 +2,113 @@ package funkin.audio.waveform; class WaveformDataParser { + static final INT16_MAX:Int = 32767; + static final INT16_MIN:Int = -32768; + + static final INT8_MAX:Int = 127; + static final INT8_MIN:Int = -128; + + public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null + { + if (sound == null) return null; + + // Method 1. This only works if the sound has been played before. + @:privateAccess + var soundBuffer:Null = sound?._channel?.__source?.buffer; + + if (soundBuffer == null) + { + // Method 2. This works if the sound has not been played before. + @:privateAccess + soundBuffer = sound?._sound?.__buffer; + + if (soundBuffer == null) + { + trace('[WAVEFORM] Failed to interpret FlxSound: ${sound}'); + return null; + } + else + { + trace('[WAVEFORM] Method 2 worked.'); + } + } + else + { + trace('[WAVEFORM] Method 1 worked.'); + } + + return interpretAudioBuffer(soundBuffer); + } + + public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null + { + var sampleRate = soundBuffer.sampleRate; + var channels = soundBuffer.channels; + var bitsPerSample = soundBuffer.bitsPerSample; + var samplesPerPoint:Int = 256; // I don't think we need to configure this. + var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant.. + + // TODO: Make this work better on HTML5. + var soundData:lime.utils.Int16Array = cast soundBuffer.data; + + var soundDataRawLength:Int = soundData.length; + var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1))); + var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint)); + + trace('Interpreting audio buffer:'); + trace(' sampleRate: ${sampleRate}'); + trace(' channels: ${channels}'); + trace(' bitsPerSample: ${bitsPerSample}'); + trace(' samplesPerPoint: ${samplesPerPoint}'); + trace(' pointsPerSecond: ${pointsPerSecond}'); + trace(' soundDataRawLength: ${soundDataRawLength}'); + trace(' soundDataSampleCount: ${soundDataSampleCount}'); + trace(' soundDataRawLength/4: ${soundDataRawLength / 4}'); + trace(' outputPointCount: ${outputPointCount}'); + + var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN; + var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX; + + var outputData:Array = []; + + for (pointIndex in 0...outputPointCount) + { + // minChannel1, maxChannel1, minChannel2, maxChannel2, ... + var values:Array = []; + + for (i in 0...channels) + { + values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX); + values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN); + } + + var rangeStart = pointIndex * samplesPerPoint; + var rangeEnd = rangeStart + samplesPerPoint; + if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount; + + for (sampleIndex in rangeStart...rangeEnd) + { + for (channelIndex in 0...channels) + { + var sampleIndex:Int = sampleIndex * channels + channelIndex; + var sampleValue = soundData[sampleIndex]; + + if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue; + if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue; + } + } + + // We now have the min and max values for the range. + for (value in values) + outputData.push(value); + } + + var outputDataLength:Int = Std.int(outputData.length / channels / 2); + var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData); + + return result; + } + public static function parseWaveformData(path:String):Null { var rawJson:String = openfl.Assets.getText(path).trim(); @@ -12,6 +119,7 @@ class WaveformDataParser { var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = false; + trace('[WAVEFORM] Parsing waveform data: ${contents}'); parser.fromJson(contents, fileName); if (parser.errors.length > 0) diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx new file mode 100644 index 000000000..46a8352d9 --- /dev/null +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -0,0 +1,244 @@ +package funkin.audio.waveform; + +import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformDataParser; +import funkin.graphics.rendering.MeshRender; +import flixel.util.FlxColor; + +class WaveformSprite extends MeshRender +{ + static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE; + static final DEFAULT_DURATION:Float = 5.0; + static final DEFAULT_ORIENTATION:WaveformOrientation = HORIZONTAL; + static final DEFAULT_X:Float = 0.0; + static final DEFAULT_Y:Float = 0.0; + static final DEFAULT_WIDTH:Float = 100.0; + static final DEFAULT_HEIGHT:Float = 100.0; + + /** + * Set this to true to tell the waveform to rebuild itself. + * Do this any time the data or drawable area of the waveform changes. + * This often (but not always) needs to be done every frame. + */ + var isWaveformDirty:Bool = true; + + public var waveformData:WaveformData; + + function set_waveformData(value:WaveformData):WaveformData + { + waveformData = value; + isWaveformDirty = true; + return waveformData; + } + + /** + * The color to render the waveform with. + */ + public var waveformColor(default, set):FlxColor; + + function set_waveformColor(value:FlxColor):FlxColor + { + waveformColor = value; + // We don't need to dirty the waveform geometry, just rebuild the texture. + rebuildGraphic(); + return waveformColor; + } + + public var orientation(default, set):WaveformOrientation; + + function set_orientation(value:WaveformOrientation):WaveformOrientation + { + orientation = value; + isWaveformDirty = true; + return orientation; + } + + /** + * Time, in seconds, at which the waveform starts. + */ + public var time(default, set):Float; + + function set_time(value:Float) + { + time = value; + isWaveformDirty = true; + return time; + } + + /** + * The duration, in seconds, that the waveform represents. + * The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents. + */ + public var duration(default, set):Float; + + function set_duration(value:Float) + { + duration = value; + isWaveformDirty = true; + return duration; + } + + /** + * Set the physical size of the waveform with `this.height = value`. + */ + override function set_height(value:Float):Float + { + isWaveformDirty = true; + return super.set_height(value); + } + + /** + * Set the physical size of the waveform with `this.width = value`. + */ + override function set_width(value:Float):Float + { + isWaveformDirty = true; + return super.set_width(value); + } + + public function new(waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + { + super(DEFAULT_X, DEFAULT_Y, DEFAULT_COLOR); + this.waveformColor = color ?? DEFAULT_COLOR; + this.width = DEFAULT_WIDTH; + this.height = DEFAULT_HEIGHT; + + this.waveformData = waveformData; + this.orientation = orientation; + this.time = 0.0; + this.duration = duration; + } + + public override function update(elapsed:Float) + { + super.update(elapsed); + + if (isWaveformDirty) + { + // Recalculate the waveform vertices. + drawWaveform(); + isWaveformDirty = false; + } + } + + function rebuildGraphic():Void + { + // The waveform is rendered using a single colored pixel as a texture. + // If you want something more elaborate, make sure to modify `build_vertex` below to use the UVs you want. + makeGraphic(1, 1, this.waveformColor); + } + + /** + * @param offsetX Horizontal offset to draw the waveform at, in samples. + */ + function drawWaveform():Void + { + // For each sample in the waveform... + // Add a MAX vertex and a MIN vertex. + // If previous MAX/MIN is empty, store. + // If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN. + // Continue until end of waveform. + + this.clear(); + + // Center point of the waveform. When horizontal this is half the height, when vertical this is half the width. + var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2); + + var oneSecondInIndices:Int = waveformData.secondsToIndex(1); + + var startTime:Float = time; + var endTime:Float = time + duration; + + var startIndex:Int = waveformData.secondsToIndex(startTime); + var endIndex:Int = waveformData.secondsToIndex(endTime); + + var pixelsPerIndex:Float = (orientation == HORIZONTAL ? this.width : this.height) / (endIndex - startIndex); + var indexesPerPixel:Float = 1 / pixelsPerIndex; + + if (pixelsPerIndex >= 1.0) + { + // Each index is at least one pixel wide, so we render each index. + var prevVertexTopIndex:Int = -1; + var prevVertexBottomIndex:Int = -1; + for (i in startIndex...endIndex) + { + var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); + + var vertexTopY:Int = Std.int(waveformCenterPos + - (waveformData.channel(0).maxSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + var vertexBottomY:Int = Std.int(waveformCenterPos + + (-waveformData.channel(0).minSampleMapped(i) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + + var vertexTopIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(pixelPos, vertexTopY) : this.build_vertex(vertexTopY, pixelPos); + var vertexBottomIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(pixelPos, vertexBottomY) : this.build_vertex(vertexBottomY, pixelPos); + + if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + { + switch (orientation) // the line of code that makes you gay + { + case HORIZONTAL: + this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); + case VERTICAL: + this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex); + } + } + + prevVertexTopIndex = vertexTopIndex; + prevVertexBottomIndex = vertexBottomIndex; + } + } + else + { + // Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it. + var prevVertexTopIndex:Int = -1; + var prevVertexBottomIndex:Int = -1; + var waveformLengthPixels:Int = orientation == HORIZONTAL ? Std.int(this.width) : Std.int(this.height); + for (i in 0...waveformLengthPixels) + { + // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. + var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); + var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); + + var vertexTopY:Int = Std.int(waveformCenterPos + - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + var vertexBottomY:Int = Std.int(waveformCenterPos + + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * (orientation == HORIZONTAL ? this.height : this.width) / 2)); + + var vertexTopIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(i, vertexTopY) : this.build_vertex(vertexTopY, i); + var vertexBottomIndex:Int = (orientation == HORIZONTAL) ? this.build_vertex(i, vertexBottomY) : this.build_vertex(vertexBottomY, i); + + if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) + { + switch (orientation) + { + case HORIZONTAL: + this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); + case VERTICAL: + this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex); + } + } + prevVertexTopIndex = vertexTopIndex; + prevVertexBottomIndex = vertexBottomIndex; + } + } + } + + public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + { + return new WaveformSprite(data, orientation, duration); + } + + public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float) + { + // TODO: Build waveform data from FunkinSound. + var data = null; + + return buildFromWaveformData(data, orientation, color, duration); + } +} + +enum WaveformOrientation +{ + HORIZONTAL; + VERTICAL; +} diff --git a/source/funkin/ui/debug/WaveformTestState.hx b/source/funkin/ui/debug/WaveformTestState.hx index b202116b9..783179c00 100644 --- a/source/funkin/ui/debug/WaveformTestState.hx +++ b/source/funkin/ui/debug/WaveformTestState.hx @@ -4,6 +4,7 @@ import flixel.FlxSprite; import flixel.util.FlxColor; import funkin.audio.FunkinSound; import funkin.audio.waveform.WaveformData; +import funkin.audio.waveform.WaveformSprite; import funkin.audio.waveform.WaveformDataParser; import funkin.graphics.rendering.MeshRender; @@ -15,128 +16,40 @@ class WaveformTestState extends MusicBeatState } var waveformData:WaveformData; + var waveformData2:WaveformData; var waveformAudio:FunkinSound; - var meshRender:MeshRender; + var waveformSprite:WaveformSprite; + // var waveformSprite2:WaveformSprite; var timeMarker:FlxSprite; public override function create():Void { super.create(); - waveformData = WaveformDataParser.parseWaveformData(Paths.json("waveform/dadbattle-erect/dadbattle-erect.waveform")); + waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect')); - waveformAudio = FunkinSound.load(Paths.music('dadbattle-erect/dadbattle-erect')); + // waveformData = WaveformDataParser.parseWaveformData(Paths.json('waveform/dadbattle-erect/dadbattle-erect.waveform')); + waveformData = WaveformDataParser.interpretFlxSound(waveformAudio); - var lightBlue:FlxColor = FlxColor.fromString("#ADD8E6"); - meshRender = new MeshRender(0, 0, lightBlue); - add(meshRender); + waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"), 5.0); + waveformSprite.width = FlxG.width; + waveformSprite.height = FlxG.height; // / 2; + add(waveformSprite); + + // waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0); + // waveformSprite2.width = FlxG.width; + // waveformSprite2.height = FlxG.height / 2; + // waveformSprite2.y = FlxG.height / 2; + // add(waveformSprite2); timeMarker = new FlxSprite(0, FlxG.height * 1 / 6); timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED); add(timeMarker); - drawWaveform(time, duration); - } - - /** - * @param offsetX Horizontal offset to draw the waveform at, in samples. - */ - function drawWaveform(timeSeconds:Float, duration:Float):Void - { - meshRender.clear(); - - var offsetX:Int = waveformData.secondsToIndex(timeSeconds); - - var waveformHeight:Int = Std.int(FlxG.height * (2 / 3)); - var waveformWidth:Int = FlxG.width; - var waveformCenterPos:Int = Std.int(FlxG.height / 2); - - var oneSecondInIndices:Int = waveformData.secondsToIndex(1); - - var startTime:Float = -1.0; - var endTime:Float = startTime + duration; - - var startIndex:Int = Std.int(offsetX + (oneSecondInIndices * startTime)); - var endIndex:Int = Std.int(offsetX + (oneSecondInIndices * (startTime + duration))); - - var pixelsPerIndex:Float = waveformWidth / (endIndex - startIndex); - var indexesPerPixel:Float = (endIndex - startIndex) / waveformWidth; - - if (pixelsPerIndex >= 1.0) - { - // Each index is at least one pixel wide, so we render each index. - var prevVertexTopIndex:Int = -1; - var prevVertexBottomIndex:Int = -1; - for (i in startIndex...endIndex) - { - var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex); - - var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleMapped(i) * waveformHeight / 2)); - var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleMapped(i) * waveformHeight / 2)); - - var vertexTopIndex:Int = meshRender.build_vertex(pixelPos, vertexTopY); - var vertexBottomIndex:Int = meshRender.build_vertex(pixelPos, vertexBottomY); - - if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) - { - meshRender.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); - } - else - { - trace('Skipping quad at index ${i}'); - } - - prevVertexTopIndex = vertexTopIndex; - prevVertexBottomIndex = vertexBottomIndex; - } - } - else - { - // Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it. - var prevVertexTopIndex:Int = -1; - var prevVertexBottomIndex:Int = -1; - for (i in 0...waveformWidth) - { - // Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming. - var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex); - var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex); - - var vertexTopY:Int = Std.int(waveformCenterPos - (waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * waveformHeight / 2)); - var vertexBottomY:Int = Std.int(waveformCenterPos + (-waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * waveformHeight / 2)); - - // trace('Drawing index ${rangeStart} at pixel ${i} with MAX ${vertexTopY} and MIN ${vertexBottomY}'); - - var vertexTopIndex:Int = meshRender.build_vertex(i, vertexTopY); - var vertexBottomIndex:Int = meshRender.build_vertex(i, vertexBottomY); - - if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1) - { - meshRender.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex); - } - else - { - trace('Skipping quad at index ${i}'); - } - - prevVertexTopIndex = vertexTopIndex; - prevVertexBottomIndex = vertexBottomIndex; - } - } - - trace('Drawing ${duration} seconds of waveform with ${meshRender.vertex_count} vertices'); - - var oneSecondInPixels:Float = waveformWidth / duration; - - timeMarker.x = Std.int(oneSecondInPixels); - - // For each sample in the waveform... - // Add a MAX vertex and a MIN vertex. - // If previous MAX/MIN is empty, store. - // If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN. - // Continue until end of waveform. + // drawWaveform(time, duration); } public override function update(elapsed:Float):Void @@ -155,46 +68,50 @@ class WaveformTestState extends MusicBeatState } } + if (FlxG.keys.justPressed.ENTER) + { + if (waveformSprite.orientation == HORIZONTAL) + { + waveformSprite.orientation = VERTICAL; + // waveformSprite2.orientation = VERTICAL; + } + else + { + waveformSprite.orientation = HORIZONTAL; + // waveformSprite2.orientation = HORIZONTAL; + } + } + if (waveformAudio.isPlaying) { - var songTimeSeconds:Float = waveformAudio.time / 1000; - drawWaveform(songTimeSeconds, duration); + // waveformSprite takes a time in fractional seconds, not milliseconds. + var timeSeconds = waveformAudio.time / 1000; + waveformSprite.time = timeSeconds; + // waveformSprite2.time = timeSeconds; } if (FlxG.keys.justPressed.UP) { - trace('Zooming out'); - duration += 1.0; - drawTheWaveform(); + waveformSprite.duration += 1.0; + // waveformSprite2.duration += 1.0; } if (FlxG.keys.justPressed.DOWN) { - trace('Zooming in'); - duration -= 1.0; - drawTheWaveform(); + waveformSprite.duration -= 1.0; + // waveformSprite2.duration -= 1.0; } if (FlxG.keys.justPressed.LEFT) { - trace('Seeking back'); - time -= 1.0; - drawTheWaveform(); + waveformSprite.time -= 1.0; + // waveformSprite2.time -= 1.0; } if (FlxG.keys.justPressed.RIGHT) { - trace('Seeking forward'); - time += 1.0; - drawTheWaveform(); + waveformSprite.time += 1.0; + // waveformSprite2.time += 1.0; } } - var time:Float = 0.0; - var duration:Float = 5.0; - - function drawTheWaveform():Void - { - drawWaveform(time, duration); - } - public override function destroy():Void { super.destroy();