# Add Color **Painter.kt** ```kotlin /** *Created by Nhien Nguyen on 8/19/2022 */ 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(DEFAULT_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 ): 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) { 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 ) } } } /** * 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 const val DEFAULT_PATCHED_SIZE = 100 } } ``` **Elastic.kt** ```kotlin import android.graphics.Color import android.util.Log import com.annhienktuit.muzikv2.enums.Interpolation import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.StretchAnimation import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.painters.Painter import com.annhienktuit.muzikv2.utils.VisualizerHelper import com.annhienktuit.muzikv2.utils.maths.polynomials.PolynomialSplineFunction /** *Created by Nhien Nguyen on 9/30/2022 */ class Elastic( private var painter: Painter, private var startHz: Int, private var endHz: Int, private var helper: VisualizerHelper, private var visualizerMaxHeight: Int, private val ampR: Float = 0.75f, var color: Int = Color.WHITE, ) { private var gravityPoints = Array(0) { Painter.GravityModel(0f) } //The rate of amplification private lateinit var fft: FloatArray // Fields for normalization private var min = 0f private var max = 0f private val highFreqDelta = 20 fun calculateCurve(updateArray: Boolean = false): PolynomialSplineFunction? { fft = helper.getFftMagnitudeRange(startHz, endHz) fft = painter.getCircleFft(fft, updateArray) minMaxNormalize(fft, visualizerMaxHeight, 0) if (gravityPoints.size != fft.size) { gravityPoints = Array(fft.size) { Painter.GravityModel(0f) } } gravityPoints.forEachIndexed { index, bar -> bar.update(fft[index] * ampR / 1.5F) } return painter.interpolateFft( gravityPoints, StretchAnimation.numberOfDot, Interpolation.SPLINE ) } /** * 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) { Log.i("Normalization", "Need normalize") for (idx in fftData.indices) { fftData[idx] = ((fftData[idx] - min) / (max - min)) * maxLimit + minLimit } } } } ``` **StretchAnimation.kt** ```kotlin import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint 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( 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) setPaintsColor() } 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 ) } points[i][numberOfDot] = points[i][0] } } } makeSureViewNotOutOfBound() for (idx in elasticBands.indices) { canvas.drawPath( polyPathsUtil.computePathThroughKnots(points[idx]), paintList[idx] ) } if (firstTimeRunning) firstTimeRunning = false } 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 setPaintsColor() { for (idx in paintList.indices) { paintList[idx].color = elasticBands[idx].color } } 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(Color.parseColor("#ffffff"), color[0], blendRatio) painter.alpha = initAlpha initAlpha -= 50 blendRatio -= 0.25f if (initAlpha < 20) initAlpha = 20 if (blendRatio <= 0) blendRatio = 0f } } 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() } } } } ``` **ParticleAnimation.kt** ```kotlin import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import com.annhienktuit.domain.models.EPointF import com.annhienktuit.muzikv2.R import com.annhienktuit.muzikv2.enums.Interpolation 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 java.util.Random import kotlin.math.PI /** *Created by Nhien Nguyen on 9/26/2022 */ class ParticleAnimation( 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, ) : Painter() { private var upperGravityPoints = Array(0) { GravityModel(0f) } private val barWidth = context.resources.getDimensionPixelSize(R.dimen.default_visualizer_bar_width).toFloat() private val radiusR: Float = .4f private val ampR: Float = 1f //The rate of amplification 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 lateinit var upperFFT: FloatArray var upperCurveUtil: PolynomialSplineFunction? = null // 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() private var actualBarHeight = 0F private var scale = 1F //Fields for bottom animator private var bottomLinePaint = Paint(Paint.ANTI_ALIAS_FLAG) lateinit var bottomFFT: FloatArray private var polyPathsUtil = PolyBezierPathUtil() var bottomCurveUtil: PolynomialSplineFunction? = null private var bottomStartHZ = 8000 private var bottomEndHz = 10000 private var bottomGravityPoints = Array(0) { GravityModel(0f) } //Fields for spears private var spearPaint = Paint(Paint.ANTI_ALIAS_FLAG) init { paint.apply { color = Color.parseColor(visualizerColor) style = Paint.Style.FILL strokeWidth = barWidth strokeCap = Paint.Cap.ROUND alpha = PAINT_ALPHA } bottomLinePaint.apply { color = Color.parseColor(visualizerColor) style = Paint.Style.STROKE strokeWidth = barWidth / 2 strokeCap = Paint.Cap.ROUND alpha = BOTTOM_PAINT_ALPHA } spearPaint.apply { color = Color.parseColor(visualizerColor) style = Paint.Style.FILL strokeWidth = barWidth / 2 strokeCap = Paint.Cap.ROUND alpha = SPEAR_PAINT_ALPHA } } override fun calc(helper: VisualizerHelper) { calcForUpperAnimator(helper) calcForBottomAnimator(helper) } private fun calcForBottomAnimator(helper: VisualizerHelper) { bottomFFT = (helper.getFftMagnitudeRange(bottomStartHZ, bottomEndHz)) bottomFFT = getCircleFft(bottomFFT) if (bottomGravityPoints.size != bottomFFT.size) { bottomGravityPoints = Array(bottomFFT.size) { GravityModel(0F) } } bottomGravityPoints.forEachIndexed { index, bar -> bar.update(bottomFFT[index] * ampR / 1.5F) } bottomCurveUtil = interpolateFft(bottomGravityPoints, numberOfDot / 5, interpolator) } private fun calcForUpperAnimator(helper: VisualizerHelper) { upperFFT = helper.getFftMagnitudeRange(startHz, endHz) upperFFT = getPowerFft(upperFFT) upperFFT = if (firstTimeRunning) { getCircleFft(upperFFT, true) } else { getCircleFft(upperFFT) } minMaxNormalize(upperFFT, visualizerMaxHeight, 0) if (upperGravityPoints.size != upperFFT.size) upperGravityPoints = Array(upperFFT.size) { GravityModel( 0f ) } upperGravityPoints.forEachIndexed { index, bar -> bar.update(upperFFT[index] * ampR) } upperCurveUtil = interpolateFftCircle(upperGravityPoints, numberOfDot, interpolator) } 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 } currentWidth = width currentHeight = height if (firstTimeRunning) { shortest = Integer.min(width, height).toFloat() adjustRadius(shortest) } drawHelperCircleLine(canvas, .5f, .5f) { if (!firstTimeRunning) { for (idx in 0 until numberOfDot) { //TODO: Replace these magic numbers upperPoints[idx] = toCartesianEpointF( shortest / 2f * (radiusR + 0.03F) + upperCurveUtil?.value(idx.toDouble())!! .toFloat(), angleOffset * idx ) bottomPoints[idx] = toCartesianEpointF( shortest / 2.4f * (radiusR + 0.03F) + bottomCurveUtil?.value(idx.toDouble())!! .toFloat(), angleOffset * idx ) } } } upperPoints[numberOfDot] = upperPoints[0] bottomPoints[numberOfDot] = bottomPoints[0] // we do this to connect the begin-end of line makeSureViewNotOutOfBound() for (idx in upperPoints.indices) { canvas.drawCircle(upperPoints[idx].x, upperPoints[idx].y, pointRadius, paint) } canvas.drawPath( polyPathsUtil.computePathThroughKnots(bottomPoints), bottomLinePaint ) drawSpears(canvas) if (firstTimeRunning) firstTimeRunning = false } private fun drawSpears(canvas: Canvas) { for (idx in bottomPoints.indices) { if (upperPoints[idx].distanceFrom(bottomPoints[idx]) > drawSpearThreshold) { canvas.drawLine( upperPoints[idx].x, upperPoints[idx].y, (bottomPoints[idx].x + upperPoints[idx].x) / 2, (bottomPoints[idx].y + upperPoints[idx].y) / 2, spearPaint ) } } } /** * Check whether the current bar is over [visualizerMaxHeight] * If true, scale tp fit [visualizerMaxHeight] */ private fun makeSureViewNotOutOfBound() { for (idx in upperPoints.indices) { actualBarHeight = (upperPoints[idx].distanceFrom(centerPoint) - discMargin - discRadius - pointRadius * 2) if (actualBarHeight > visualizerMaxHeight) { scale = visualizerMaxHeight / actualBarHeight upperPoints[idx] = upperPoints[idx].scaleBy(scale) } } } /** * 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)) val currentRadius = (firstPoint.first - secondPoint.first) / 2 val newRadius: Float if (currentRadius < discRadius) { newRadius = discRadius.toFloat() + discMargin val ratio = (newRadius / currentRadius) this.shortest *= ratio } } /** * 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) } } } } /** * Handle high frequency value to avoid low variability * If that data is in range [[visualizerMaxHeight] - [highFreqDelta] , [visualizerMaxHeight]] */ private fun handleHighFreqValue(idx: Int) { try { if (frequencyMap.containsKey(idx)) { frequencyMap[idx] = frequencyMap[idx]!!.plus(1) } else { frequencyMap[idx] = 1 } if (frequencyMap[idx]!! > 3) { frequencyMap[idx] = 0 upperFFT[idx] = (random.nextInt((visualizerMaxHeight - highFreqDelta) - highFreqDelta + 1) + highFreqDelta).toFloat() } } catch (e: Exception) { e.printStackTrace() } } /** * 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 } } private fun Float.isExclusive(start: Int, stop: Int): Boolean { return (this >= start && this <= stop) } private fun getShortest(): Float = Integer.min(currentWidth, currentHeight).toFloat() override fun setPaintColor(vararg color: Int) { if(color.isEmpty()) return this.paint.color = color[0] this.bottomLinePaint.color = color[0] if(color.size >= 2) this.spearPaint.color = color[1] else this.spearPaint.color = color[0] paint.alpha = PAINT_ALPHA bottomLinePaint.alpha = BOTTOM_PAINT_ALPHA spearPaint.alpha = SPEAR_PAINT_ALPHA } companion object { private const val TAG = "ParticleAnimation" private const val numberOfDot: Int = 100 private const val drawSpearThreshold = 20F private const val pointRadius = 4f private const val visualizerColor = "#FFFFFF" private const val PAINT_ALPHA = 200 private const val BOTTOM_PAINT_ALPHA = 175 private const val SPEAR_PAINT_ALPHA = 30 val upperPoints = Array(numberOfDot + 1) { EPointF() } val bottomPoints = Array(numberOfDot + 1) { EPointF() } val centerPoint = EPointF(0F, 0F) } } ```