---
# System prepended metadata

title: BUILD 10/10

---

# BUILD 10/10

**FFTVisualizer**
```kotlin
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.util.Log
import android.view.View
import com.annhienktuit.muzikv2.R
import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.painters.Painter
import com.annhienktuit.muzikv2.utils.VisualizerHelper

/**
 *Created by Nhien Nguyen on 8/19/2022
 */
class FFTVisualizer : View {
    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 visibility = true
    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.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
        }
        Log.i("OnMeasure", "MaxHeight $size $reMeasuredFreeSpace , $visualizerMaxHeight and visibility=$visibility")
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (!this::visualizerHelper.isInitialized) return
        handleOnFirstRun(canvas, painter)
        if ((width > 0 && height > 0 && visibility)) {
            if (isPlaying) {
                painter.calc(visualizerHelper)
                painter.draw(canvas, width, height)
                postInvalidateDelayed(REFRESH_TIME_60FPS)
            } else {
                Log.i("DrawStatus", "Drawing w o calc")
                painter.draw(canvas, width, height)
            }
        } else {
            Log.e("DrawStatus", "Cannot draw because height = $height & width = $width")
        }
    }

    /**
     * Handle initialize and drawing when the view create
     */
    private fun handleOnFirstRun(canvas: Canvas, painter: Painter) {
        when (painter) {
            is CircleLineAnimation -> {
                if (painter.curveUtil == null) {
                    //If saved data exist, draw it first
                    if (!CircleLineAnimation.points.isZeroArray()) {
             //           painter.draw(canvas, width, height, true)
                    }
                    //We must run this function on 1st time to initialize curve utils
                    painter.calc(visualizerHelper)
                }
            }
            is ParticleAnimation -> {
                if (painter.upperCurveUtil == null ||
                    painter.bottomCurveUtil == null
                ) {
                    //If saved data exist, draw it first
                    if(!painter.isZeroEPointFArray(ParticleAnimation.upperPoints)){
   //                     painter.draw(canvas, width, height, true)
                    }
                    painter.calc(visualizerHelper)
                }
            }
            is StretchAnimation -> {
                if(painter.isCurveUtilNull()){
                    painter.calc(visualizerHelper)
                }
            }
        }
    }

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

    /**
     * Notify new playing status to [CircleLineAnimation]
     */
    private fun updatePainterPlayStatus(playing: Boolean) {
        painter.isPlaying = playing
    }

    fun onResume() {
        visibility = true
        invalidate()
    }

    fun onPause() {
        visibility = false
    }

    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
    }
}

```

**StretchAnimation**

```kotlin
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.Log
import androidx.core.graphics.ColorUtils
import com.annhienktuit.domain.models.EPointF
import com.annhienktuit.muzikv2.R
import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.models.Elastic
import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.painters.Painter
import com.annhienktuit.muzikv2.utils.VisualizerHelper
import com.annhienktuit.muzikv2.utils.audiovisualizer.PolyBezierPathUtil
import com.annhienktuit.muzikv2.utils.maths.polynomials.PolynomialSplineFunction
import kotlin.math.PI
import kotlin.properties.Delegates

/**
 *Created by Nhien Nguyen on 9/30/2022
 */
class StretchAnimation @JvmOverloads constructor(
    var context: Context,
    override var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG),
) : Painter() {
    private var elasticBands = arrayOf<Elastic>()
    var elasticCurveUtils = arrayOfNulls<PolynomialSplineFunction>(4)
    private var polyPathsUtil = PolyBezierPathUtil()
    private var paintList = Array(numberOfBand) { Paint(Paint.ANTI_ALIAS_FLAG) }
    private val barWidth =
        context.resources.getDimensionPixelSize(R.dimen.default_visualizer_bar_width).toFloat()
    private val radiusR: Float = .4f
    private val angleOffset = 2 * PI.toFloat() / numberOfDot
    private var shortest = 0F
    override var discRadius = 0
        set(value) {
            field = value
            shortest = getShortest()
            adjustRadius(shortest)
        }
    override var isPlaying = false
    var firstTimeRunning = true
    private var currentHeight = 0
    private var currentWidth = 0
    private var currentRadius by Delegates.notNull<Float>()
    private var actualBarHeight = 0F
    private var scale = 1F
    var defaultBlendColor = Color.parseColor("#707070")

    init {
        for (paint in paintList) {
            paint.apply {
                style = Paint.Style.STROKE
                strokeWidth = barWidth / 2
                strokeCap = Paint.Cap.ROUND
                alpha = 150
            }
        }
    }

    override fun calc(helper: VisualizerHelper) {
        if (elasticBands.size < numberOfBand) {
            initElasticBands(helper)
        }
        Log.i(TAG, "empty ${isCurveUtilNull()}")
        for (idx in elasticBands.indices) {
            if (firstTimeRunning) {
                elasticCurveUtils[idx] = elasticBands[idx].calculateCurve(true)!!
            } else {
                elasticCurveUtils[idx] = elasticBands[idx].calculateCurve(false)!!
            }
        }
    }

    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
            }
            return
        }
        if (firstTimeRunning) {
            shortest = Integer.min(width, height).toFloat()
            adjustRadius(shortest)
            currentWidth = width
            currentHeight = height
        }
        drawHelperCircleLine(canvas, .5f, .5f) {
            if (!firstTimeRunning) {
                for (i in elasticBands.indices) {
                    for (j in 0 until numberOfDot) {
                        points[i][j] = toCartesianEpointF(
                            shortest / 2.2f * (radiusR + 0.03F) + elasticCurveUtils[i]?.value(j.toDouble())
                            !!.toFloat(), angleOffset * j
                        )
                    }
                }
            }
        }
        connectSmoothPaths()
        makeSureViewNotOutOfBound()
        for (idx in elasticBands.indices) {
            canvas.drawPath(
                polyPathsUtil.computePathThroughKnots(points[idx]), paintList[idx]
            )
        }
        if (firstTimeRunning) firstTimeRunning = false
    }

    private fun connectSmoothPaths() {
        for (idx in elasticBands.indices) {
            points[idx][0] = points[idx][numberOfDot - 1]
            points[idx][numberOfDot - 1] = points[idx][0]
            points[idx][numberOfDot] = points[idx][1]
        }
    }

    private fun makeSureViewNotOutOfBound() {
        for (i in elasticBands.indices) {
            for (j in 0 until numberOfDot) {
                actualBarHeight =
                    (points[i][j].distanceFrom(centerPoint) - discMargin - discRadius - pointRadius * 2)
                if (actualBarHeight > visualizerMaxHeight) {
                    scale = visualizerMaxHeight / actualBarHeight
                    points[i][j] = points[i][j].scaleBy(scale)
                }
            }
        }
    }

    private fun initElasticBands(helper: VisualizerHelper) {
        elasticBands = arrayOf(
            Elastic(
                this,
                20,
                2000,
                helper,
                visualizerMaxHeight,
                1.25f,
            ),
            Elastic(
                this,
                2000,
                4000,
                helper,
                visualizerMaxHeight
            ),
            Elastic(
                this,
                4000,
                6000,
                helper,
                visualizerMaxHeight,
            ),
            Elastic(
                this,
                6000,
                10000,
                helper,
                visualizerMaxHeight,
            )
        )
    }

    /**
     * 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 * (numberOfDot / 2))
        currentRadius = (firstPoint.first - secondPoint.first) / 2
        val newRadius: Float
        if (currentRadius < discRadius) {
            newRadius = discRadius.toFloat() + discMargin
            val ratio = (newRadius / currentRadius)
            this.shortest *= ratio
        }
    }

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

    override fun setPaintColor(vararg color: Int) {
        var initAlpha = 250
        var blendRatio = 1f
        for (painter in paintList) {
            painter.color = ColorUtils.blendARGB(defaultBlendColor, color[0], blendRatio)
            painter.alpha = initAlpha
            initAlpha -= 50
            blendRatio -= 0.25f
            if (initAlpha < 20) initAlpha = 20
            if (blendRatio <= 0) blendRatio = 0f
        }
    }

    fun isCurveUtilNull(): Boolean = elasticCurveUtils.any { it == null }

    companion object {
        private const val TAG = "StretchAnimation"
        private const val pointRadius = 4f
        private val centerPoint = EPointF(0F, 0F)
        private const val numberOfBand: Int = 4
        const val numberOfDot: Int = 100
        val points = Array(numberOfBand) {
            Array(numberOfDot + 1) {
                EPointF()
            }
        }
    }
}
```

Painter
```kotlin
import android.graphics.Canvas
import android.graphics.Paint
import android.util.Log
import androidx.annotation.ColorInt
import com.annhienktuit.domain.models.EPointF
import com.annhienktuit.muzikv2.enums.Interpolation
import com.annhienktuit.muzikv2.utils.VisualizerHelper
import com.annhienktuit.muzikv2.utils.maths.interpolation.AkimaSplineInterpolator
import com.annhienktuit.muzikv2.utils.maths.interpolation.LinearInterpolator
import com.annhienktuit.muzikv2.utils.maths.polynomials.PolynomialSplineFunction
import java.util.Arrays
import kotlin.math.cos
import kotlin.math.sin

abstract class Painter {
    open var visualizerMaxHeight = 0
    open var discRadius = 0
    open var discMargin = 0
    open var isPlaying = false
    abstract var paint: Paint
    private val li = LinearInterpolator()
    private val sp = AkimaSplineInterpolator()
    private val quietThreshold = 5f
    private var patched: FloatArray = FloatArray(PATCHED_SIZE)
    private var psf: PolynomialSplineFunction? = null
    private var nRaw: Int = 0
    private var xRaw = DoubleArray(0)
    private var yRaw = DoubleArray(0)

    //Fields for normalization
    private var max = 0F
    private var min = 0F

    open fun setPaintColor(@ColorInt vararg color: Int) {
        if(color.isEmpty()) return
        this.paint.color = color[0]
    }

    /**
     * An abstract function that every painters must implement and do their calculation there.
     *
     * @param helper the visualizerHelper from VisualizerView
     */
    abstract fun calc(helper: VisualizerHelper)

    /**
     * An abstract function that every painters must implement and do their drawing there.
     *
     * @param canvas the canvas from VisualizerView
     */
    abstract fun draw(canvas: Canvas, width: Int = 0, height: Int = 0, drawWithoutCalc: Boolean = false)

    /**
     * Interpolate FFT spectrum
     *
     * Android don't capture a high resolution spectrum, and we want the number of bands be adjustable,
     * so we do an interpolation here.
     *
     * (For example, to show 64 bands at frequencies from 0Hz to 1200Hz, Android only return ~10 FFT values.
     * So we need to interpolate those ~10 values into 64 values in order to fit it into our bands)
     *
     * @param gravityModels Array of gravityModel
     * @param sliceNum Number of Slice
     * @param interpolator Which interpolator to use, `li` for Linear, `sp` for Spline
     *
     * @return a `PolynomialSplineFunction` (psf). To get the value, use
     * `psf.value(x)`, where `x` must be a Double value from 0 to `num`
     */
    fun interpolateFft(
        gravityModels: Array<GravityModel>, sliceNum: Int, interpolator: Interpolation = Interpolation.SPLINE
    ): PolynomialSplineFunction? {
        if (gravityModels.size != nRaw) {
            nRaw = gravityModels.size
            xRaw = DoubleArray(nRaw) { (it * sliceNum).toDouble() / (nRaw - 1) }
            yRaw = DoubleArray(nRaw)
        } else {
            xRaw.forEach {
                (it * sliceNum) / (nRaw - 1)
            }
        }
        gravityModels.forEachIndexed { index, bar -> yRaw[index] = bar.height.toDouble() }
        psf = when (interpolator) {
            Interpolation.LINEAR -> li.interpolate(xRaw, yRaw)
            Interpolation.SPLINE -> sp.interpolate(xRaw, yRaw)
        }
        return psf
    }

    /**
     * Interpolate FFT spectrum (Circle)
     *
     * Similar to `interpolateFft()`. However this is meant for Fft from `getCircleFft()`
     *
     * @param gravityModels Array of gravityModel
     * @param sliceNum Number of Slice
     * @param interpolator Which interpolator to use, `li` for Linear, `sp` for Spline
     *
     * @return a `PolynomialSplineFunction` (psf). To get the value, use
     * `psf.value(x)`, where `x` must be a Double value from 0 to `num`
     */
    fun interpolateFftCircle(
        gravityModels: Array<GravityModel>, sliceNum: Int, interpolator: Interpolation
    ): PolynomialSplineFunction? {
        if (gravityModels.size != nRaw) {
            nRaw = gravityModels.size
            xRaw = DoubleArray(nRaw) { ((it - 1) * sliceNum).toDouble() / (nRaw - 1 - 2) }
            yRaw = DoubleArray(nRaw)
        } else {
            xRaw.forEach {
                ((it - 1) * sliceNum) / (nRaw - 1 - 2)
            }
        }
        gravityModels.forEachIndexed { index, bar -> yRaw[index] = bar.height.toDouble() }
        psf = when (interpolator) {
            Interpolation.LINEAR -> li.interpolate(xRaw, yRaw)
            Interpolation.SPLINE -> sp.interpolate(xRaw, yRaw)
        }
        return psf
    }

    /**
     * Check if it's quiet enough such that we can skip the drawing
     * @param fft Fft
     * @return true if it's quiet, false otherwise
     */
    fun isQuiet(fft: FloatArray): Boolean {
        fft.forEach { if (it > quietThreshold) return false }
        Log.i("IsQuiet", "True")
        return true
    }

    /**
     * Check if it's silence enough?
     * If true, we suppress all to 0 for easier visualizer
     */
    fun isSilence(fft: FloatArray): Boolean {
        if(fft.isEmpty()) return false
        if(getAverage(fft) > LOW_VALUE_THRESHOLD) return false
        return true
    }

    private fun getAverage(fft: FloatArray):Float {
        var sum = 0F
        for(idx in fft.indices){
            sum += fft[idx]
        }
        return sum / fft.size
    }

    fun suppressLowValue(fft: FloatArray){
        Arrays.fill(fft, 0F)
    }

    /**
     * Convert Polar to Cartesian
     * @param radius Radius
     * @param theta Theta
     * @return Pair of (x,y) of Cartesian
     */
    fun toCartesian(radius: Float, theta: Float): Pair<Float, Float> {
        return Pair(radius * cos(theta), radius * sin(theta))
    }

    /**
     * Convert Polar to Cartesian
     * @param radius Radius
     * @param theta Theta
     * @return [EPointF] of (x,y) of Cartesian
     */
    fun toCartesianEpointF(radius: Float, theta: Float): EPointF{
        return EPointF(radius * cos(theta), radius * sin(theta))
    }

    /**
     * Patch the Fft so that the start and the end connect perfectly. Use this with `interpolateFftCircle()`
     *
     * `[0, 1, ..., n] -> [n-1, 0, 1, ..., n-1, 0, 1]`
     *
     * @param fft Fft
     * @return CircleFft
     */
    fun getCircleFft(fft: FloatArray, updateArraySize: Boolean = false): FloatArray {
        if (updateArraySize || fft.size != PATCHED_SIZE) {
            PATCHED_SIZE = fft.size
            patched = FloatArray(fft.size + 2)
        }
        fft.forEachIndexed { index, item -> patched[index + 1] = item }
        patched[0] = fft[fft.lastIndex - 1]
        patched[patched.lastIndex - 1] = fft[0]
        patched[patched.lastIndex] = fft[1]
        return patched
    }

    /**
     * Boost high values while suppress low values, generally give a powerful feeling
     * @param fft Fft
     * @param power Parameter, adjust to fit your liking
     * @return PowerFft
     */
    fun getPowerFft(fft: FloatArray, power: Float = 100.0F): FloatArray {
        return fft.map { it * it / power }.toFloatArray()
    }

    /**
     * Draw the circle visualizer
     * @param xR The ratio of X position to the canvas.width (= the width of the visualizer's view)
     * For example, 0f mean the start/left, while 1f mean the end/right of the screen.
     * @param yR The ratio of Y position to the canvas.height (= the height of the visualizer's view)
     * For example, 0f mean the top, while 1f mean the bottom of the screen.
     */
    fun drawHelperCircleLine(
        canvas: Canvas, xR: Float, yR: Float, drawCircleLine: () -> Unit
    ) {
        canvas.save()
        canvas.translate(canvas.width * xR, canvas.height * yR)
        drawCircleLine()
    }

    fun minMaxNormalizeEpointF(points: Array<EPointF>, maxLimit: Int, minLimit: Int = 0){
        min = Float.MAX_VALUE
        max = Float.MIN_VALUE
        for(point in points){
            if(point.x > max) max = point.x
            if(point.x < min) min = point.x
        }
        // We only do normalization if max element data is reached out limit
        if(max > maxLimit){
            for(idx in points.indices){
                points[idx].setX(
                    ((points[idx].x - min) / (max - min)) * maxLimit + minLimit
                )
            }
        }
    }

    fun isZeroEPointFArray(points: Array<EPointF>): Boolean{
        if (points.isEmpty()) return true
        points.forEach { if(it.x != 0F || it.y != 0F) return false }
        return true
    }

    /**
     * A model with gravity. Useful to smooth raw Fft values.
     */
    class GravityModel(
        var height: Float = 0f,
        var dy: Float = 0f,
        var ay: Float = 2f
    ) {
        fun update(h: Float) {
            if (h > height) {
                height = h
                dy = 0f
            }
            height -= dy
            dy += ay
            if (height < 0) {
                height = 0f
                dy = 0f
            }
        }

        override fun toString(): String {
            return "@GravityModel[Height: $height, dy: $dy, ay: $ay]"
        }
    }

    companion object{
        private const val LOW_VALUE_THRESHOLD = 7F
        private var PATCHED_SIZE = 100
    }
}
```