#BUILD 06/09
**FFTCircleLine.kt**
```java
/**
*Created by Nhien Nguyen on 8/19/2022
*/
class FFTCircleLine(
var context: Context,
override var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG),
var startHz: Int = 20,
var endHz: Int = 2000,
var interpolator: Interpolation = Interpolation.SPLINE,
var discMargin: Int = 0
) : Painter() {
private var gravityPoints = Array(0) { GravityModel(0f) }
private val barWidth =
context.resources.getDimensionPixelSize(R.dimen.default_visualizer_bar_width).toFloat()
private val numberOfBar: Int = 100
private val debugPaint = Paint()
private val radiusR: Float = .4f
private val ampR: Float = 1f
private val points = FloatArray(4 * numberOfBar)
private val angleOffset = 2 * PI.toFloat() / numberOfBar
private var shortest = 0F
var discRadius = 0
set(value) {
field = value
shortest = getShortest()
adjustRadius(shortest)
}
var isFirstTimeRunning = true
var visualizerMaxHeight = 0
var curveUtil: PolynomialSplineFunction? = null
lateinit var fft: FloatArray
// Fields for normalization
private var min = 0f
private var max = 0f
private val highFreqDelta = 20
private var frequencyMap = HashMap<Int, Int>()
private var random = Random()
// Fields for draw
private var start = Pair(0F, 0F)
private var stop = Pair(0F, 0F)
private var currentHeight: Int = 0
private var currentWidth: Int = 0
init {
paint.apply {
color = Color.WHITE
style = Paint.Style.FILL
strokeWidth = barWidth
strokeCap = Paint.Cap.ROUND
}
debugPaint.apply {
isAntiAlias = true
color = Color.RED
style = Paint.Style.FILL
strokeWidth = barWidth
}
}
override fun calc(helper: VisualizerHelper) {
fft = helper.getFftMagnitudeRange(startHz, endHz)
minMaxNormalize(fft, visualizerMaxHeight, 0)
fft = getPowerFft(fft)
fft = if (isFirstTimeRunning) {
getCircleFft(fft, true)
} else {
getCircleFft(fft)
}
if (gravityPoints.size != fft.size) gravityPoints = Array(fft.size) { GravityModel(0f) }
gravityPoints.forEachIndexed { index, bar -> bar.update(fft[index] * ampR) }
curveUtil = interpolateFftCircle(gravityPoints, numberOfBar, interpolator)
}
/**
* Apply min-max normalization then handle if its value is "high frequency" continously
* @see <a href="https://towardsdatascience.com/everything-you-need-to-know-about-min-max-normalization-in-python-b79592732b79">Min-Max Normalization </a>
* A data is defined to be "high frequency" if its value is in range [[visualizerMaxHeight - [highFreqDelta]] , [visualizerMaxHeight]]
*/
private fun minMaxNormalize(fftData: FloatArray, maxLimit: Int, minLimit: Int = 0) {
min = Float.MAX_VALUE
max = Float.MIN_VALUE
for (value in fftData) {
if (value > max) max = value
if (value < min) min = value
}
if (max > visualizerMaxHeight) {
for (idx in fftData.indices) {
fftData[idx] = ((fftData[idx] - min) / (max - min)) * maxLimit + minLimit
if (fftData[idx].isExclusive(visualizerMaxHeight - highFreqDelta, visualizerMaxHeight)) {
handleHighFreqValue(idx)
} else {
resetMapValue(idx)
}
}
}
}
/**
* If the frequency value is not continue to return high frequency, reset it counter in [frequencyMap]
*/
private fun resetMapValue(idx: Int) {
if (frequencyMap.containsKey(idx) && frequencyMap[idx]!! > 0) {
frequencyMap[idx] = 0
}
}
/**
* Handle high frequency value to avoid low variability
* If that data is in range [ [visualizerMaxHeight] - [highFreqDelta] , [visualizerMaxHeight]]
*/
private fun handleHighFreqValue(idx: Int) {
if (frequencyMap.containsKey(idx)) {
frequencyMap[idx] = frequencyMap[idx]!!.plus(1)
} else {
frequencyMap[idx] = 1
}
if (frequencyMap[idx]!! > 3) {
frequencyMap[idx] = 0
fft[idx] = (random.nextInt((visualizerMaxHeight - highFreqDelta) - highFreqDelta + 1) + highFreqDelta).toFloat()
Log.i("RefactorData", "Refactored to ${fft[idx]}")
}
}
private fun getShortest(): Float = min(currentWidth, currentHeight).toFloat()
override fun draw(canvas: Canvas, width: Int, height: Int) {
currentWidth = width
currentHeight = height
if (isFirstTimeRunning) {
shortest = min(width, height).toFloat()
adjustRadius(shortest)
isFirstTimeRunning = false
}
drawHelperCircleLine(canvas, .5f, .5f) {
for (i in 0 until numberOfBar) {
start = toCartesian(shortest / 2f * radiusR, angleOffset * i)
stop = toCartesian(
shortest / 2f * radiusR + curveUtil?.value(i.toDouble())!!.toFloat(),
angleOffset * i
)
points[4 * i] = start.first
points[4 * i + 1] = start.second
points[4 * i + 2] = stop.first
points[4 * i + 3] = stop.second
}
canvas.drawLines(points, paint)
}
}
/**
* Calculate the new radius between 2 opposite point in circle
* Then scale [shortest] based on ratio between new radius and old radius
*/
private fun adjustRadius(minValue: Float) {
val firstPoint = toCartesian(minValue / 2f * radiusR, angleOffset * 0)
val secondPoint = toCartesian(minValue / 2f * radiusR, angleOffset * (numberOfBar / 2))
val currentRadius = (firstPoint.first - secondPoint.first) / 2
val newRadius: Float
if (currentRadius < discRadius) {
newRadius = discRadius.toFloat() + discMargin
val ratio = (newRadius / currentRadius)
this.shortest *= ratio
}
}
/**
* @return if number is in range [[start], [stop]]
*/
private fun Float.isExclusive(start: Int, stop: Int): Boolean {
return (this >= start && this <= stop)
}
}
```