---
# System prepended metadata

title: BUILD 12/9

---

# BUILD 12/9


**FFTCircleLine.kt**
```kotlin
class FFTCircleLine(
    var context: Context,
    override var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG),
    private var startHz: Int = 20,
    private var endHz: Int = 2000,
    private 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 debugPaint = Paint()
    private val radiusR: Float = .4f
    private val ampR: Float = 1f //The rate of amplification
    private val angleOffset = 2 * PI.toFloat() / numberOfBar
    private var shortest = 0F
    var discRadius = 0
        set(value) {
            field = value
            shortest = getShortest()
            adjustRadius(shortest)
        }
    var firstTimeRunning = true
    var isPlaying = false
    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) {
        Log.i("CalcStatus", "Calculating on ${Thread.currentThread().id}")
        fft = helper.getFftMagnitudeRange(startHz, endHz)
        Log.i("FFTArray_Raw", fft.contentToStringWithIndex())
        fft = getPowerFft(fft)
        fft = if (firstTimeRunning) {
            getCircleFft(fft, true)
        } else {
            getCircleFft(fft)
        }
        Log.i("FFTArray", fft.contentToStringWithIndex())
        minMaxNormalize(fft, visualizerMaxHeight, 0)
        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" continuously
     * @see <a href="https://towardsdatascience.com/everything-you-need-to-know-about-min-max-normalization-in-python-b79592732b79">Min-Max Normalization </a> <br>
     * <p> A data is defined to be "high frequency" if its value is in range <br> [[visualizerMaxHeight] - [highFreqDelta] , [visualizerMaxHeight]] </p>
     */
    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()
        }
    }

    override fun draw(canvas: Canvas, width: Int, height: Int, drawWithoutCalc: Boolean) {
        if (drawWithoutCalc) {
//            drawHelperCircleLine(canvas, .5f, .5f) {
//                // Still need to run this function to do the translate, but we don't update data on the first time
//                // Otherwise, it will draw in somewhere psychically
//            }
//            canvas.drawLines(points, paint)
            return
        }
        currentWidth = width
        currentHeight = height
        if (firstTimeRunning) {
            shortest = min(width, height).toFloat()
            adjustRadius(shortest)
        }
        drawHelperCircleLine(canvas, .5f, .5f) {
            if (!firstTimeRunning) {
                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
                }
            }
        }
        if (!drawWithoutCalc) canvas.drawLines(points, paint)
        if (firstTimeRunning) firstTimeRunning = false
    }

    /**
     * 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)
    }

    private fun FloatArray.contentToStringWithIndex(): String {
        val strBuilder = StringBuilder()
        strBuilder.append("[")
        for (index in 0 until this.size) {
            strBuilder.append(" $index: ${this[index]},")
        }
        strBuilder.append("]")
        return strBuilder.toString()
    }

    private fun getShortest(): Float = min(currentWidth, currentHeight).toFloat()

    companion object {
        private const val numberOfBar: Int = 100
        val points = FloatArray(4 * numberOfBar)
    }
}
```

**FFTVisualizer.kt**
```kotlin
class FFTVisualizer : View {
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val discMargin = context.resources.getDimensionPixelSize(R.dimen.default_disc_margin)
    private val barWidth =
        context.resources.getDimensionPixelSize(R.dimen.default_visualizer_bar_width)
    private var visualizerMaxHeight: Int =
        context.resources.getDimensionPixelSize(R.dimen.default_visualizer_max_height)
    private lateinit var painter: Painter
    private lateinit var visualizerHelper: VisualizerHelper
    private var reMeasuredFreeSpace = 0
    var visible = false
    var isPlaying = false
    var discRadius: Int = 0

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    fun setup(visualizerHelper: VisualizerHelper, painter: Painter) {
        this.visualizerHelper = visualizerHelper
        this.painter = painter.also {
            (it as? FFTCircleLine)?.visualizerMaxHeight = this.visualizerMaxHeight
        }
        this.onResume()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val size = measuredWidth.coerceAtMost(measuredHeight)
        setMeasuredDimension(size, size)
        reMeasuredFreeSpace = ((size - discRadius * 2 - discMargin * 2 - barWidth * 4) / 2)
        if (reMeasuredFreeSpace < visualizerMaxHeight) visualizerMaxHeight = reMeasuredFreeSpace
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (!this::visualizerHelper.isInitialized) return
        if ((painter as? FFTCircleLine)?.curveUtil == null) {
            //If saved data exist, draw it first
            if (!FFTCircleLine.points.isZeroArray() && !isPlaying){
                painter.draw(canvas, width, height, true)
            }
            //We must run this function on 1st time to initialize curve utils
            painter.calc(visualizerHelper)
        }
        if ((width > 0 && height > 0 && visible)) {
            if (isPlaying) {
                painter.calc(visualizerHelper)
                painter.draw(canvas, width, height)
                postInvalidateDelayed(REFRESH_TIME_60FPS)
            } else {
                Log.i("DrawStatus", "Skip Calculating")
                painter.draw(canvas, width, height)
            }
        } else {
            Log.e("DrawStatus", "Cannot draw because height = $height & width = $width")
        }
    }

    fun onResume() {
        visible = true
        setLayerType(LAYER_TYPE_HARDWARE, paint)
        invalidate()
    }

    fun onPause() {
        visible = false
        setLayerType(LAYER_TYPE_SOFTWARE, null)
    }

    /**
     * Show the visualizer
     */
    fun visible() {
        visible = true
    }

    /**
     * Hide the visualizer
     */
    fun invisible() {
        visible = false
    }

    fun setPlayingStatus(isPlaying: Boolean) {
        this.isPlaying = isPlaying
        if (isPlaying) {
            invalidate()
        }
        updatePainterPlayStatus(this.isPlaying)
    }

    /**
     * Notify new playing status to [FFTCircleLine]
     */
    private fun updatePainterPlayStatus(playing: Boolean) {
        (painter as? FFTCircleLine)?.let {
            it.isPlaying = playing
        }
    }

    private fun FloatArray.isZeroArray(): Boolean{
        for(idx in 0 until this.size){
            if(this[idx] != 0F) return false
        }
        return true
    }

    companion object {
        private const val REFRESH_TIME_60FPS: Long = 1000 / 60
    }
}
```
