# BUILD 26/08 **Remove Dependency** ```grad implementation 'org.apache.commons:commons-math3:3.6.1' ``` **Add these utils to replace the dependency** [DOWNLOAD LINK](https://www.file.io/7va3/download/DcWHWLujUkVp) **FFTCircleLine.kt** ```kotlin /** *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 var skipFrame = false 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 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 // Fields for draw private var start = Pair(0F, 0F) private var stop = Pair(0F, 0F) init { paint.apply { color = Color.WHITE style = Paint.Style.FILL strokeWidth = barWidth } 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) if (isQuiet(fft)) { skipFrame = true return } else skipFrame = false fft = getPowerFft(fft) fft = if (isFirstTimeRunning) { getCircleFft(fft, true) } else { getCircleFft(fft) } Log.i("RawFFT", fft.contentToString()) 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) Log.i("Knots ", curveUtil?.polynomials.contentToString()) } 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 } } } ``` **Painter.kt** ```kotlin abstract class Painter { private val li = LinearInterpolator() private val sp = AkimaSplineInterpolator() private val quietThreshold = 5f private var patched: FloatArray = FloatArray(0) private var psf: PolynomialSplineFunction? = null 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() } 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() } 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 } /** * 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 } /** * 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() } 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 } } } } ``` **FFTVisualizer** ```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 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 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 - barWidth * 4) / 2) if (reMeasuredMaxHeight < visualizerMaxHeight) visualizerMaxHeight = reMeasuredMaxHeight } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (!this::visualizerHelper.isInitialized) return if (enable || (painter as? FFTCircleLine)?.curveUtil == null ) { painter.calc(visualizerHelper) } if (width > 0 && height > 0) { painter.draw(canvas, width, height) } postInvalidateDelayed(1000 / 60) } fun onResume() { enable = true setLayerType(LAYER_TYPE_HARDWARE, paint) invalidate() } fun onPause() { enable = false setLayerType(LAYER_TYPE_SOFTWARE, null) } } ``` **VisualizerHelper** ```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 = FFTAudioProcessor.FFTListener { sampleRateHz, channelCount, fft -> fftBuffer = fft } } 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)) } private fun hzToFftIndex(Hz: Int): Int { return Math.min(Math.max(Hz * 1024 / (44100 * 2), 0), 255) } /** * Release visualizer when not using anymore */ fun release() { fftM = emptyArray<Float>().toFloatArray() fftBuffer = emptyArray<Float>().toFloatArray() } } ```