#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) } } ```