---
# System prepended metadata

title: Build 24/8

---

# Build 24/8

**FFTVisualizer.kt**
```kotlin
/**
 *Created by Nhien Nguyen on 8/19/2022
 */
class FFTVisualizer : View {
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val discMargin = context.resources.getDimensionPixelSize(R.dimen.default_disc_margin)
    private var visualizerMaxHeight: Int =
        context.resources.getDimensionPixelSize(R.dimen.default_visualizer_max_height).toInt()
    private lateinit var painter: Painter
    private lateinit var visualizerHelper: VisualizerHelper
    private var reMeasuredMaxHeight: Int = 0
    var enable = false
    var discRadius: Int = 0
    var time: Long = System.currentTimeMillis()

    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
            (it as? FFTCircleLine)?.visualizerMaxHeight = visualizerMaxHeight
        }
        this.onResume()
    }

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

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (!this::visualizerHelper.isInitialized) return
        if(enable){
            setLayerType(LAYER_TYPE_HARDWARE, paint)
            painter.calc(visualizerHelper)
        }
        if (width > 0 && height > 0) {
            painter.draw(canvas, width, height)
        }
        postInvalidateDelayed(1000 / 60)
    }

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

    fun onPause() {
        enable = false
    }

}
```

**FFTCircleLine**
```kotlin
/**
 *Created by Nhien Nguyen on 8/19/2022
 */
class FFTCircleLine(
    override var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG),
    //
    var startHz: Int = 20,
    var endHz: Int = 2000,
    //
    var interpolator: Interpolation = Interpolation.SPLINE,
    //
    var power: Boolean = true,
    //
    var discMargin: Int = 0
) : Painter() {
    private var gravityPoints = Array(hzToFftIndex(endHz) - hzToFftIndex(startHz)) { GravityModel(0f) }
    private var skipFrame = false
    private val barWidth = 8f
    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
    var isFirstTimeRunning = true
    var visualizerMaxHeight = 0
    lateinit var fft: FloatArray
    lateinit var curveUtil: PolynomialSplineFunction

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

    // Fields for draw
    private var start = Pair(0F, 0F)
    private var stop = Pair(0F, 0F)
//    var defaultCurveUtil: PolynomialSplineFunction

    init {
        paint.apply {
            color = Color.WHITE
            style = Paint.Style.FILL
            strokeWidth = barWidth
        }
        debugPaint.apply {
            color = Color.RED
            style = Paint.Style.FILL
            strokeWidth = barWidth
        }
//          defaultCurveUtil = interpolateFftCircle(gravityPoints, numberOfBar, interpolator)
    }

    override fun calc(helper: VisualizerHelper) {
        fft = helper.getFftMagnitudeRange(startHz, endHz)
        minMaxNormalize(fft, visualizerMaxHeight, 0)
        if (isQuiet(fft)) {
            skipFrame = true
            return
        } else skipFrame = false

        if (power) 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)
    }

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

    override fun draw(canvas: Canvas, width: Int, height: Int) {
        if (skipFrame) {
            drawBottomBar(canvas, start, stop)
            return
        }
        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
                drawBottomBar(canvas, start, stop)
            }
            canvas.drawLines(points, paint)
        }
    }

    private fun drawBottomBar(canvas: Canvas, start: Pair<Float, Float>, stop: Pair<Float, Float>){
        canvas.drawCircle(start.first, start.second, barWidth / 2, paint) //draw floor
        canvas.drawCircle(stop.first, stop.second, barWidth / 2, paint) // draw ceil
    }

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

**VisualizerHelper.kt**
```kotlin
**
 *Created by Nhien Nguyen on 8/19/2022
 */
class VisualizerHelper {

    private var fftM: FloatArray
    private var fftBuffer: FloatArray
    private var listener: FFTAudioProcessor.FFTListener
    private lateinit var audioProcessor: FFTAudioProcessor

    init {
        fftBuffer = FloatArray(FFTAudioProcessor.SAMPLE_SIZE + 2)
        fftM = FloatArray(fftBuffer.size / 2 - 1)
        listener = object : FFTAudioProcessor.FFTListener{
            override fun onFFTReady(sampleRateHz: Int, channelCount: Int, fft: FloatArray) {
                fftBuffer = fft
            }

            override fun onBitrateChange(bitrateFactor: Int) {
                // Will be implemented soon or maybe delete if unnecessary
            }
        }
    }

    fun setAudioProcessor(processor: FFTAudioProcessor){
        this.audioProcessor = processor
        processor.listener = listener
    }

    fun getFftBuffer(): FloatArray {
        return fftBuffer
    }

    private fun getFftMagnitude(): FloatArray {
        for(k in fftM.indices){
            val i = (k+1) * 2
            fftM[k] = hypot(fftBuffer[i], fftBuffer[i+1])
        }
        return fftM
    }

    /**
     * Get Fft values from startHz to endHz
     */
    fun getFftMagnitudeRange(startHz: Int, endHz: Int): FloatArray {
        return getFftMagnitude().copyOfRange(hzToFftIndex(startHz), hzToFftIndex(endHz))
    }



    /**
     * Release visualizer when not using anymore
     */
    fun release() {
        fftM = emptyArray<Float>().toFloatArray()
        fftBuffer = emptyArray<Float>().toFloatArray()
    }

    companion object{
        /**
         * Equation from documentation, kth frequency = k*Fs/(n/2)
         */
        fun hzToFftIndex(Hz: Int): Int {
            return Math.min(Math.max(Hz * 1024 / (44100 * 2), 0), 255)
        }
    }

}
```

**Painter.kt**
```kotlin
abstract class Painter {

    private val li = LinearInterpolator()
    private val sp = AkimaSplineInterpolator()
    private var patched: FloatArray = FloatArray(0)
    abstract var paint: Paint

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

    /**
     * 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: String
    ): PolynomialSplineFunction {
        val nRaw = gravityModels.size
        val xRaw = DoubleArray(nRaw) { (it * sliceNum).toDouble() / (nRaw - 1) }
        val yRaw = DoubleArray(nRaw)
        gravityModels.forEachIndexed { index, bar -> yRaw[index] = bar.height.toDouble() }
        val psf: PolynomialSplineFunction
        psf = when (interpolator) {
            "li", "linear" -> li.interpolate(xRaw, yRaw)
            "sp", "spline" -> sp.interpolate(xRaw, yRaw)
            else -> li.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 {
        val nRaw = gravityModels.size
        val xRaw = DoubleArray(nRaw) { ((it - 1) * sliceNum).toDouble() / (nRaw - 1 - 2) }
        val yRaw = DoubleArray(nRaw)
        gravityModels.forEachIndexed { index, bar -> yRaw[index] = bar.height.toDouble() }
        val psf: PolynomialSplineFunction =
            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 {
        val threshold = 5f
        fft.forEach { if (it > threshold) return false }
        Log.i("IsQuiet", "True")
        return true
    }

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

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

    /**
     * Patch the Fft to a MirrorFft
     *
     * @param fft Fft
     * @param mode when 0 -> do nothing
     *             when 1 ->
     *              `[0, 1, ..., n] -> [n, ..., 1, 0, 0, 1, ..., n]`
     *             when 2 ->
     *              `[0, 1, ..., n] -> [0, 1, ..., n, n, ..., 1, 0]`
     *             when 3 ->
     *             `[0, 1, ..., n] -> [n/2, ..., 1, 0, 0, 1, ..., n/2]`
     *             when 4 ->
     *             `[0, 1, ..., n] -> [0, 1, ..., n/2, n/2, ..., 1, 0]`
     * @return MirrorFft
     */
    fun getMirrorFft(fft: FloatArray, mode: Int = 1): FloatArray {
        return when (mode) {
            1 -> {
                fft.sliceArray(0..fft.lastIndex).reversedArray() + fft.sliceArray(0..fft.lastIndex)
            }
            2 -> {
                fft.sliceArray(0..fft.lastIndex) + fft.sliceArray(0..fft.lastIndex).reversedArray()
            }
            3 -> {
                fft.sliceArray(0..fft.lastIndex / 2)
                    .reversedArray() + fft.sliceArray(0..fft.lastIndex / 2)
            }
            4 -> {
                fft.sliceArray(0..fft.lastIndex / 2) + fft.sliceArray(0..fft.lastIndex / 2)
                    .reversedArray()
            }
            else -> fft
        }
    }

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

    fun drawHelperCircleLine(
        canvas: Canvas, xR: Float, yR: Float, drawCircleLine: () -> Unit
    ) {
        canvas.save()
        canvas.translate(canvas.width * xR, canvas.height * yR)
        drawCircleLine()
    }

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

**dimens.xml**
```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="default_disc_margin">12dp</dimen>
    <dimen name="default_visualizer_max_height">36dp</dimen>
</resources>
```