mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-01-19 02:58:14 +00:00
Performant audio waveforms generated directly from provided FlxSound elements.
This commit is contained in:
parent
c3d2582252
commit
e8fa7f9c70
|
@ -5,6 +5,8 @@ import funkin.util.MathUtil;
|
||||||
@:nullSafety
|
@:nullSafety
|
||||||
class WaveformData
|
class WaveformData
|
||||||
{
|
{
|
||||||
|
static final DEFAULT_VERSION:Int = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the waveform data format.
|
* The version of the waveform data format.
|
||||||
* @default `2` (-1 if not specified/invalid)
|
* @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.
|
* Lower values can more accurately represent the waveform when zoomed in, but take more data.
|
||||||
*/
|
*/
|
||||||
@:alias('samples_per_pixel')
|
@: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`.
|
* 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;
|
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.
|
* Array of Int16 values representing the waveform.
|
||||||
|
@ -46,7 +48,16 @@ class WaveformData
|
||||||
@:jignored
|
@:jignored
|
||||||
var channelData:Null<Array<WaveformDataChannel>> = null;
|
var channelData:Null<Array<WaveformDataChannel>> = null;
|
||||||
|
|
||||||
public function new() {}
|
public function new(?version:Int, channels:Int, sampleRate:Int, samplesPerPoint:Int, bits:Int, length:Int, data:Array<Int>)
|
||||||
|
{
|
||||||
|
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<WaveformDataChannel>
|
function buildChannelData():Array<WaveformDataChannel>
|
||||||
{
|
{
|
||||||
|
@ -79,7 +90,7 @@ class WaveformData
|
||||||
*/
|
*/
|
||||||
public function maxSampleValue():Int
|
public function maxSampleValue():Int
|
||||||
{
|
{
|
||||||
if (_maxSampleValue != -1) return _maxSampleValue;
|
if (_maxSampleValue != 0) return _maxSampleValue;
|
||||||
return _maxSampleValue = Std.int(Math.pow(2, bits));
|
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.
|
* Cache the value because `Math.pow` is expensive and the value gets used a lot.
|
||||||
*/
|
*/
|
||||||
@:jignored
|
@:jignored
|
||||||
var _maxSampleValue:Int = -1;
|
var _maxSampleValue:Int = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The length of the waveform in samples.
|
* @return The length of the waveform in samples.
|
||||||
*/
|
*/
|
||||||
public function lenSamples():Int
|
public function lenSamples():Int
|
||||||
{
|
{
|
||||||
return length * samplesPerPixel;
|
return length * samplesPerPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,7 +121,7 @@ class WaveformData
|
||||||
*/
|
*/
|
||||||
public function secondsToIndex(seconds:Float):Int
|
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
|
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;
|
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<Int> = 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
|
class WaveformDataChannel
|
||||||
{
|
{
|
||||||
var parent:WaveformData;
|
var parent:WaveformData;
|
||||||
|
@ -149,6 +210,9 @@ class WaveformDataChannel
|
||||||
this.channelId = channelId;
|
this.channelId = channelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a given minimum point at an index.
|
||||||
|
*/
|
||||||
public function minSample(i:Int)
|
public function minSample(i:Int)
|
||||||
{
|
{
|
||||||
var offset = (i * parent.channels + this.channelId) * 2;
|
var offset = (i * parent.channels + this.channelId) * 2;
|
||||||
|
@ -165,7 +229,7 @@ class WaveformDataChannel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum value within the range of samples.
|
* 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)
|
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.
|
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||||
* @param i
|
|
||||||
*/
|
*/
|
||||||
public function minSampleRangeMapped(start:Int, end:Int)
|
public function minSampleRangeMapped(start:Int, end:Int)
|
||||||
{
|
{
|
||||||
return minSampleRange(start, end) / parent.maxSampleValue();
|
return minSampleRange(start, end) / parent.maxSampleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a given maximum point at an index.
|
||||||
|
*/
|
||||||
public function maxSample(i:Int)
|
public function maxSample(i:Int)
|
||||||
{
|
{
|
||||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||||
|
@ -203,7 +269,7 @@ class WaveformDataChannel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum value within the range of samples.
|
* 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)
|
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.
|
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||||
* @param i
|
|
||||||
*/
|
*/
|
||||||
public function maxSampleRangeMapped(start:Int, end:Int)
|
public function maxSampleRangeMapped(start:Int, end:Int)
|
||||||
{
|
{
|
||||||
return maxSampleRange(start, end) / parent.maxSampleValue();
|
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)
|
public function setMinSample(i:Int, value:Int)
|
||||||
{
|
{
|
||||||
var offset = (i * parent.channels + this.channelId) * 2;
|
var offset = (i * parent.channels + this.channelId) * 2;
|
||||||
|
|
|
@ -2,6 +2,113 @@ package funkin.audio.waveform;
|
||||||
|
|
||||||
class WaveformDataParser
|
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<WaveformData>
|
||||||
|
{
|
||||||
|
if (sound == null) return null;
|
||||||
|
|
||||||
|
// Method 1. This only works if the sound has been played before.
|
||||||
|
@:privateAccess
|
||||||
|
var soundBuffer:Null<lime.media.AudioBuffer> = 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<WaveformData>
|
||||||
|
{
|
||||||
|
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<Int> = [];
|
||||||
|
|
||||||
|
for (pointIndex in 0...outputPointCount)
|
||||||
|
{
|
||||||
|
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
|
||||||
|
var values:Array<Int> = [];
|
||||||
|
|
||||||
|
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<WaveformData>
|
public static function parseWaveformData(path:String):Null<WaveformData>
|
||||||
{
|
{
|
||||||
var rawJson:String = openfl.Assets.getText(path).trim();
|
var rawJson:String = openfl.Assets.getText(path).trim();
|
||||||
|
@ -12,6 +119,7 @@ class WaveformDataParser
|
||||||
{
|
{
|
||||||
var parser = new json2object.JsonParser<WaveformData>();
|
var parser = new json2object.JsonParser<WaveformData>();
|
||||||
parser.ignoreUnknownVariables = false;
|
parser.ignoreUnknownVariables = false;
|
||||||
|
trace('[WAVEFORM] Parsing waveform data: ${contents}');
|
||||||
parser.fromJson(contents, fileName);
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
if (parser.errors.length > 0)
|
if (parser.errors.length > 0)
|
||||||
|
|
244
source/funkin/audio/waveform/WaveformSprite.hx
Normal file
244
source/funkin/audio/waveform/WaveformSprite.hx
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import flixel.FlxSprite;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import funkin.audio.FunkinSound;
|
import funkin.audio.FunkinSound;
|
||||||
import funkin.audio.waveform.WaveformData;
|
import funkin.audio.waveform.WaveformData;
|
||||||
|
import funkin.audio.waveform.WaveformSprite;
|
||||||
import funkin.audio.waveform.WaveformDataParser;
|
import funkin.audio.waveform.WaveformDataParser;
|
||||||
import funkin.graphics.rendering.MeshRender;
|
import funkin.graphics.rendering.MeshRender;
|
||||||
|
|
||||||
|
@ -15,128 +16,40 @@ class WaveformTestState extends MusicBeatState
|
||||||
}
|
}
|
||||||
|
|
||||||
var waveformData:WaveformData;
|
var waveformData:WaveformData;
|
||||||
|
var waveformData2:WaveformData;
|
||||||
|
|
||||||
var waveformAudio:FunkinSound;
|
var waveformAudio:FunkinSound;
|
||||||
|
|
||||||
var meshRender:MeshRender;
|
var waveformSprite:WaveformSprite;
|
||||||
|
|
||||||
|
// var waveformSprite2:WaveformSprite;
|
||||||
var timeMarker:FlxSprite;
|
var timeMarker:FlxSprite;
|
||||||
|
|
||||||
public override function create():Void
|
public override function create():Void
|
||||||
{
|
{
|
||||||
super.create();
|
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");
|
waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"), 5.0);
|
||||||
meshRender = new MeshRender(0, 0, lightBlue);
|
waveformSprite.width = FlxG.width;
|
||||||
add(meshRender);
|
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 = new FlxSprite(0, FlxG.height * 1 / 6);
|
||||||
timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED);
|
timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED);
|
||||||
add(timeMarker);
|
add(timeMarker);
|
||||||
|
|
||||||
drawWaveform(time, duration);
|
// 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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override function update(elapsed:Float):Void
|
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)
|
if (waveformAudio.isPlaying)
|
||||||
{
|
{
|
||||||
var songTimeSeconds:Float = waveformAudio.time / 1000;
|
// waveformSprite takes a time in fractional seconds, not milliseconds.
|
||||||
drawWaveform(songTimeSeconds, duration);
|
var timeSeconds = waveformAudio.time / 1000;
|
||||||
|
waveformSprite.time = timeSeconds;
|
||||||
|
// waveformSprite2.time = timeSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FlxG.keys.justPressed.UP)
|
if (FlxG.keys.justPressed.UP)
|
||||||
{
|
{
|
||||||
trace('Zooming out');
|
waveformSprite.duration += 1.0;
|
||||||
duration += 1.0;
|
// waveformSprite2.duration += 1.0;
|
||||||
drawTheWaveform();
|
|
||||||
}
|
}
|
||||||
if (FlxG.keys.justPressed.DOWN)
|
if (FlxG.keys.justPressed.DOWN)
|
||||||
{
|
{
|
||||||
trace('Zooming in');
|
waveformSprite.duration -= 1.0;
|
||||||
duration -= 1.0;
|
// waveformSprite2.duration -= 1.0;
|
||||||
drawTheWaveform();
|
|
||||||
}
|
}
|
||||||
if (FlxG.keys.justPressed.LEFT)
|
if (FlxG.keys.justPressed.LEFT)
|
||||||
{
|
{
|
||||||
trace('Seeking back');
|
waveformSprite.time -= 1.0;
|
||||||
time -= 1.0;
|
// waveformSprite2.time -= 1.0;
|
||||||
drawTheWaveform();
|
|
||||||
}
|
}
|
||||||
if (FlxG.keys.justPressed.RIGHT)
|
if (FlxG.keys.justPressed.RIGHT)
|
||||||
{
|
{
|
||||||
trace('Seeking forward');
|
waveformSprite.time += 1.0;
|
||||||
time += 1.0;
|
// waveformSprite2.time += 1.0;
|
||||||
drawTheWaveform();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var time:Float = 0.0;
|
|
||||||
var duration:Float = 5.0;
|
|
||||||
|
|
||||||
function drawTheWaveform():Void
|
|
||||||
{
|
|
||||||
drawWaveform(time, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override function destroy():Void
|
public override function destroy():Void
|
||||||
{
|
{
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
|
Loading…
Reference in a new issue