2024-01-20 14:07:48 -05:00
|
|
|
package funkin.audio.waveform;
|
|
|
|
|
|
|
|
import funkin.util.MathUtil;
|
|
|
|
|
|
|
|
@:nullSafety
|
|
|
|
class WaveformData
|
|
|
|
{
|
2024-01-23 22:47:27 -05:00
|
|
|
static final DEFAULT_VERSION:Int = 2;
|
|
|
|
|
2024-01-20 14:07:48 -05:00
|
|
|
/**
|
|
|
|
* The version of the waveform data format.
|
|
|
|
* @default `2` (-1 if not specified/invalid)
|
|
|
|
*/
|
|
|
|
public var version(default, null):Int = -1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The number of channels in the waveform.
|
|
|
|
*/
|
|
|
|
public var channels(default, null):Int = 1;
|
|
|
|
|
|
|
|
@:alias('sample_rate')
|
|
|
|
public var sampleRate(default, null):Int = 44100;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of input audio samples per output waveform data point.
|
|
|
|
* At base zoom level this is number of samples per pixel.
|
|
|
|
* Lower values can more accurately represent the waveform when zoomed in, but take more data.
|
|
|
|
*/
|
|
|
|
@:alias('samples_per_pixel')
|
2024-01-23 22:47:27 -05:00
|
|
|
public var samplesPerPoint(default, null):Int = 256;
|
2024-01-20 14:07:48 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of bits to use for each sample value. Valid values are `8` and `16`.
|
|
|
|
*/
|
|
|
|
public var bits(default, null):Int = 16;
|
|
|
|
|
|
|
|
/**
|
2024-01-23 22:47:27 -05:00
|
|
|
* The length of the data array, in points.
|
2024-01-20 14:07:48 -05:00
|
|
|
*/
|
2024-01-23 22:47:27 -05:00
|
|
|
public var length(default, null):Int = 0;
|
2024-01-20 14:07:48 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Array of Int16 values representing the waveform.
|
|
|
|
* TODO: Use an `openfl.Vector` for performance.
|
|
|
|
*/
|
|
|
|
public var data(default, null):Array<Int> = [];
|
|
|
|
|
|
|
|
@:jignored
|
|
|
|
var channelData:Null<Array<WaveformDataChannel>> = null;
|
|
|
|
|
2024-01-23 22:47:27 -05:00
|
|
|
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;
|
|
|
|
}
|
2024-01-20 14:07:48 -05:00
|
|
|
|
|
|
|
function buildChannelData():Array<WaveformDataChannel>
|
|
|
|
{
|
|
|
|
channelData = [];
|
|
|
|
for (i in 0...channels)
|
|
|
|
{
|
|
|
|
channelData.push(new WaveformDataChannel(this, i));
|
|
|
|
}
|
|
|
|
return channelData;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function channel(index:Int)
|
|
|
|
{
|
|
|
|
return (channelData == null) ? buildChannelData()[index] : channelData[index];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function get(index:Int):Int
|
|
|
|
{
|
|
|
|
return data[index] ?? 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function set(index:Int, value:Int)
|
|
|
|
{
|
|
|
|
data[index] = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum possible value for a waveform data point.
|
|
|
|
* The minimum possible value is (-1 * maxSampleValue)
|
|
|
|
*/
|
|
|
|
public function maxSampleValue():Int
|
|
|
|
{
|
2024-01-23 22:47:27 -05:00
|
|
|
if (_maxSampleValue != 0) return _maxSampleValue;
|
2024-01-20 14:07:48 -05:00
|
|
|
return _maxSampleValue = Std.int(Math.pow(2, bits));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cache the value because `Math.pow` is expensive and the value gets used a lot.
|
|
|
|
*/
|
|
|
|
@:jignored
|
2024-01-23 22:47:27 -05:00
|
|
|
var _maxSampleValue:Int = 0;
|
2024-01-20 14:07:48 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return The length of the waveform in samples.
|
|
|
|
*/
|
|
|
|
public function lenSamples():Int
|
|
|
|
{
|
2024-01-23 22:47:27 -05:00
|
|
|
return length * samplesPerPoint;
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return The length of the waveform in seconds.
|
|
|
|
*/
|
|
|
|
public function lenSeconds():Float
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline lenSamples() / sampleRate;
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given the time in seconds, return the waveform data point index.
|
|
|
|
*/
|
|
|
|
public function secondsToIndex(seconds:Float):Int
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return Std.int(seconds * inline pointsPerSecond());
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a waveform data point index, return the time in seconds.
|
|
|
|
*/
|
|
|
|
public function indexToSeconds(index:Int):Float
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return index / inline pointsPerSecond();
|
2024-01-23 22:47:27 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The number of data points this waveform data provides per second of audio.
|
|
|
|
*/
|
|
|
|
public inline function pointsPerSecond():Float
|
|
|
|
{
|
|
|
|
return sampleRate / samplesPerPoint;
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given the percentage progress through the waveform, return the waveform data point index.
|
|
|
|
*/
|
|
|
|
public function percentToIndex(percent:Float):Int
|
|
|
|
{
|
|
|
|
return Std.int(percent * length);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a waveform data point index, return the percentage progress through the waveform.
|
|
|
|
*/
|
|
|
|
public function indexToPercent(index:Int):Float
|
|
|
|
{
|
|
|
|
return index / length;
|
|
|
|
}
|
2024-01-23 22:47:27 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2024-02-09 14:58:57 -05:00
|
|
|
/**
|
|
|
|
* Create a new WaveformData whose data represents the two waveforms overlayed.
|
|
|
|
*/
|
|
|
|
public function merge(that:WaveformData):WaveformData
|
|
|
|
{
|
|
|
|
var result = this.clone([]);
|
|
|
|
|
|
|
|
for (channelIndex in 0...this.channels)
|
|
|
|
{
|
|
|
|
var thisChannel = this.channel(channelIndex);
|
|
|
|
var thatChannel = that.channel(channelIndex);
|
|
|
|
var resultChannel = result.channel(channelIndex);
|
|
|
|
|
|
|
|
for (index in 0...this.length)
|
|
|
|
{
|
|
|
|
var thisMinSample = thisChannel.minSample(index);
|
|
|
|
var thatMinSample = thatChannel.minSample(index);
|
|
|
|
|
|
|
|
var thisMaxSample = thisChannel.maxSample(index);
|
|
|
|
var thatMaxSample = thatChannel.maxSample(index);
|
|
|
|
|
|
|
|
resultChannel.setMinSample(index, Std.int(Math.min(thisMinSample, thatMinSample)));
|
|
|
|
resultChannel.setMaxSample(index, Std.int(Math.max(thisMaxSample, thatMaxSample)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@:privateAccess
|
|
|
|
result.length = this.length;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2024-01-23 22:47:27 -05:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
2024-01-23 22:47:27 -05:00
|
|
|
@:nullSafety
|
2024-01-20 14:07:48 -05:00
|
|
|
class WaveformDataChannel
|
|
|
|
{
|
|
|
|
var parent:WaveformData;
|
|
|
|
var channelId:Int;
|
|
|
|
|
|
|
|
public function new(parent:WaveformData, channelId:Int)
|
|
|
|
{
|
|
|
|
this.parent = parent;
|
|
|
|
this.channelId = channelId;
|
|
|
|
}
|
|
|
|
|
2024-01-23 22:47:27 -05:00
|
|
|
/**
|
|
|
|
* Retrieve a given minimum point at an index.
|
|
|
|
*/
|
2024-01-20 14:07:48 -05:00
|
|
|
public function minSample(i:Int)
|
|
|
|
{
|
|
|
|
var offset = (i * parent.channels + this.channelId) * 2;
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline parent.get(offset);
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mapped to a value between 0 and 1.
|
|
|
|
*/
|
|
|
|
public function minSampleMapped(i:Int)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline minSample(i) / inline parent.maxSampleValue();
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Minimum value within the range of samples.
|
2024-01-23 22:47:27 -05:00
|
|
|
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
2024-01-20 14:07:48 -05:00
|
|
|
*/
|
|
|
|
public function minSampleRange(start:Int, end:Int)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
var min = inline parent.maxSampleValue();
|
2024-01-20 14:07:48 -05:00
|
|
|
for (i in start...end)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
var sample = inline minSample(i);
|
2024-01-20 14:07:48 -05:00
|
|
|
if (sample < min) min = sample;
|
|
|
|
}
|
|
|
|
return min;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
|
|
|
*/
|
|
|
|
public function minSampleRangeMapped(start:Int, end:Int)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline minSampleRange(start, end) / inline parent.maxSampleValue();
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
2024-01-23 22:47:27 -05:00
|
|
|
/**
|
|
|
|
* Retrieve a given maximum point at an index.
|
|
|
|
*/
|
2024-01-20 14:07:48 -05:00
|
|
|
public function maxSample(i:Int)
|
|
|
|
{
|
|
|
|
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline parent.get(offset);
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mapped to a value between 0 and 1.
|
|
|
|
*/
|
|
|
|
public function maxSampleMapped(i:Int)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline maxSample(i) / inline parent.maxSampleValue();
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum value within the range of samples.
|
2024-01-23 22:47:27 -05:00
|
|
|
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
2024-01-20 14:07:48 -05:00
|
|
|
*/
|
|
|
|
public function maxSampleRange(start:Int, end:Int)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
var max = -(inline parent.maxSampleValue());
|
2024-01-20 14:07:48 -05:00
|
|
|
for (i in start...end)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
var sample = inline maxSample(i);
|
2024-01-20 14:07:48 -05:00
|
|
|
if (sample > max) max = sample;
|
|
|
|
}
|
|
|
|
return max;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
|
|
|
*/
|
|
|
|
public function maxSampleRangeMapped(start:Int, end:Int)
|
|
|
|
{
|
2024-02-02 21:53:45 -05:00
|
|
|
return inline maxSampleRange(start, end) / inline parent.maxSampleValue();
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
public function setMinSample(i:Int, value:Int)
|
|
|
|
{
|
|
|
|
var offset = (i * parent.channels + this.channelId) * 2;
|
2024-02-02 21:53:45 -05:00
|
|
|
inline parent.set(offset, value);
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
public function setMaxSample(i:Int, value:Int)
|
|
|
|
{
|
|
|
|
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
2024-02-02 21:53:45 -05:00
|
|
|
inline parent.set(offset, value);
|
2024-01-20 14:07:48 -05:00
|
|
|
}
|
|
|
|
}
|