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