package funkin.audio.waveform; 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) */ 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') public var samplesPerPoint(default, null):Int = 256; /** * Number of bits to use for each sample value. Valid values are `8` and `16`. */ public var bits(default, null):Int = 16; /** * The length of the data array, in points. */ public var length(default, null):Int = 0; /** * Array of Int16 values representing the waveform. * TODO: Use an `openfl.Vector` for performance. */ public var data(default, null):Array = []; @:jignored var channelData:Null> = null; 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 { 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 { if (_maxSampleValue != 0) return _maxSampleValue; return _maxSampleValue = Std.int(Math.pow(2, bits)); } /** * Cache the value because `Math.pow` is expensive and the value gets used a lot. */ @:jignored var _maxSampleValue:Int = 0; /** * @return The length of the waveform in samples. */ public function lenSamples():Int { return length * samplesPerPoint; } /** * @return The length of the waveform in seconds. */ public function lenSeconds():Float { return inline lenSamples() / sampleRate; } /** * Given the time in seconds, return the waveform data point index. */ public function secondsToIndex(seconds:Float):Int { return Std.int(seconds * inline pointsPerSecond()); } /** * Given a waveform data point index, return the time in seconds. */ public function indexToSeconds(index:Int):Float { return index / inline pointsPerSecond(); } /** * The number of data points this waveform data provides per second of audio. */ public inline function pointsPerSecond():Float { return sampleRate / samplesPerPoint; } /** * 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; } /** * 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 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; } /** * 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; var channelId:Int; public function new(parent:WaveformData, channelId:Int) { this.parent = parent; this.channelId = channelId; } /** * Retrieve a given minimum point at an index. */ public function minSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2; return inline parent.get(offset); } /** * Mapped to a value between 0 and 1. */ public function minSampleMapped(i:Int) { return inline minSample(i) / inline parent.maxSampleValue(); } /** * Minimum value within the range of samples. * NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead. */ public function minSampleRange(start:Int, end:Int) { var min = inline parent.maxSampleValue(); for (i in start...end) { var sample = inline minSample(i); 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) { return inline minSampleRange(start, end) / inline parent.maxSampleValue(); } /** * Retrieve a given maximum point at an index. */ public function maxSample(i:Int) { var offset = (i * parent.channels + this.channelId) * 2 + 1; return inline parent.get(offset); } /** * Mapped to a value between 0 and 1. */ public function maxSampleMapped(i:Int) { return inline maxSample(i) / inline parent.maxSampleValue(); } /** * Maximum value within the range of samples. * NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead. */ public function maxSampleRange(start:Int, end:Int) { var max = -(inline parent.maxSampleValue()); for (i in start...end) { var sample = inline maxSample(i); 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) { return inline maxSampleRange(start, end) / inline parent.maxSampleValue(); } public function setMinSample(i:Int, value:Int) { var offset = (i * parent.channels + this.channelId) * 2; inline parent.set(offset, value); } public function setMaxSample(i:Int, value:Int) { var offset = (i * parent.channels + this.channelId) * 2 + 1; inline parent.set(offset, value); } }