# BUILD 23/8 **Dependencies** implementation 'com.github.paramsen:noise:2.0.0' implementation 'org.apache.commons:commons-math3:3.6.1' **VisualizerHelper.kt** ```java /** *Created by Nhien Nguyen on 8/19/2022 */ import android.util.Log import com.annhienktuit.muzikv2.processors.FFTAudioProcessor import kotlin.math.hypot class VisualizerHelper() { private val fftMF: FloatArray private val fftM: FloatArray private var fftBuffer: FloatArray private var listener: FFTAudioProcessor.FFTListener private lateinit var audioProcessor: FFTAudioProcessor init { fftBuffer = FloatArray(FFTAudioProcessor.SAMPLE_SIZE + 2) fftMF = FloatArray(fftBuffer.size / 2 - 1) 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) { } } } fun setAudioProcessor(processor: FFTAudioProcessor){ this.audioProcessor = processor processor.listener = listener } fun getFftBuffer(): FloatArray { return fftBuffer } fun getFftMagnitude(): FloatArray { getFftBuffer() for(k in fftMF.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 { val sIndex = hzToFftIndex(startHz) val eIndex = hzToFftIndex(endHz) Log.i("getFftMagnitudeRange", "Getting in range $sIndex -> $eIndex") return getFftMagnitude().copyOfRange(sIndex, eIndex) } /** * Equation from documentation, kth frequency = k*Fs/(n/2) */ 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() { } } ``` **FFTVisualizer.kt** ```java import android.content.Context import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.util.AttributeSet import android.view.View import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.painters.Painter import com.annhienktuit.muzikv2.utils.FrameManager import com.annhienktuit.muzikv2.utils.VisualizerHelper /** *Created by Nhien Nguyen on 8/19/2022 */ class FFTVisualizer: View { private val frameManager = FrameManager() private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private lateinit var painter: Painter private lateinit var visualizerHelper: VisualizerHelper var enable = false var discRadius = 0 companion object { private fun dp2px(resources: Resources, dp: Float): Float { return dp * resources.displayMetrics.density } } 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 this.onResume() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val size = measuredWidth.coerceAtMost(measuredHeight) setMeasuredDimension(size, size) } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) if(!this::visualizerHelper.isInitialized) return if(!enable) { painter.draw(canvas!!, visualizerHelper, width, height) invalidate() } else { setLayerType(LAYER_TYPE_HARDWARE, paint) canvas?.apply { painter.calc(visualizerHelper) if (width > 0 && height > 0) { painter.draw(canvas, visualizerHelper, width, height) } } frameManager.tick() if (enable) invalidate() } } fun onResume(){ enable = true invalidate() } fun onPause(){ enable = false } } ``` **VisualizerLayout** ```java import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout import com.annhienktuit.muzikv2.R import com.annhienktuit.muzikv2.ui.views.visualizer.DiscArtworkView import com.annhienktuit.muzikv2.utils.VisualizerHelper /** *Created by Nhien Nguyen on 8/19/2022 */ class VisualizerLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0, ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) { var discArtworkView: DiscArtworkView var visualizerView: FFTVisualizer var discRadius: Int init { inflate(context, R.layout.visualizer_layout, this) discArtworkView = findViewById(R.id.discArtworkView) visualizerView = findViewById(R.id.fftVisualizerView) discRadius = discArtworkView.radius } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val size = measuredWidth.coerceAtMost(measuredHeight) setMeasuredDimension(size, size) val reMeasureMaxHeight = ((size - discRadius * 2 - FFTCircleLine.discMargin * 2)/2).toInt() if(reMeasureMaxHeight < visualizerMaxHeight) visualizerMaxHeight = reMeasureMaxHeight } fun setup(helper: VisualizerHelper){ val painter = FFTCircleLine() painter.discRadius = this.discRadius visualizerView.setup(helper, painter) } fun enable(){ visualizerView.enable = true } fun disable(){ visualizerView.enable = false } companion object{ var visualizerMaxHeight: Int = 120 } } ``` **FFTCircleLine** ```java import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import com.annhienktuit.muzikv2.App import com.annhienktuit.muzikv2.enums.Interpolation import com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.painters.Painter import com.annhienktuit.muzikv2.utils.ViewUtil import com.annhienktuit.muzikv2.utils.VisualizerHelper import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction import java.lang.Integer.min import kotlin.math.PI /** *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 mirror: Boolean = false, var power: Boolean = true, // ) : Painter() { private var gravityPoints = Array(0) { GravityModel() } private var skipFrame = false lateinit var fft: FloatArray lateinit var curveUtil: PolynomialSplineFunction private val barWidth = 8f private var shortest = 0F val points = FloatArray(4 * numberOfBar) val debugPaint = Paint() var discRadius = 0 var isFirstTimeRunning = true init { paint.apply { color = Color.WHITE style = Paint.Style.FILL strokeWidth = barWidth } debugPaint.apply { color = Color.RED style = Paint.Style.FILL strokeWidth = barWidth } } override fun calc(helper: VisualizerHelper) { fft = helper.getFftMagnitudeRange(startHz, endHz) minMaxNormalize(fft, VisualizerLayout.visualizerMaxHeight, 0) if (isQuiet(fft)) { skipFrame = true return } else skipFrame = false if (power) fft = getPowerFft(fft) fft = if (mirror) getMirrorFft(fft) 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) { var min = Float.MAX_VALUE var max = Float.MIN_VALUE for (value in fftData) { if (value > max) max = value if (value < min) min = value } if (max > VisualizerLayout.visualizerMaxHeight) { for (idx in fftData.indices) { fftData[idx] = ((fftData[idx] - min) / (max - min)) * maxLimit + minLimit } } } override fun draw(canvas: Canvas, helper: VisualizerHelper, width: Int, height: Int) { if (skipFrame) return if (isFirstTimeRunning) { shortest = min(width, height).toFloat() adjustRadius(shortest) isFirstTimeRunning = false } drawHelperCircleLine(canvas, .5f, .5f) { for (i in 0 until numberOfBar) { val start = toCartesian(shortest / 2f * radiusR, angleOffset * i) val 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 canvas.drawCircle(start.first, start.second, barWidth / 2, paint) //draw floor canvas.drawCircle(stop.first, stop.second, barWidth / 2, paint) // draw ceil } canvas.drawLines(points, paint) } } 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 var newRadius: Float if (currentRadius < discRadius) { newRadius = discRadius.toFloat() + discMargin val ratio = (newRadius/currentRadius) this.shortest *= ratio } } companion object { const val radiusR: Float = .4f const val ampR: Float = 1f const val numberOfBar: Int = 100 private const val angleOffset = 2 * PI.toFloat() / numberOfBar var discMargin = ViewUtil.convertDpToPixel(App.getInstance().applicationContext, 12F) } } ``` **Painter.kt** ```java /** *Created by Nhien Nguyen on 8/19/2022 */ import android.graphics.Canvas import android.graphics.Paint import android.util.Log import com.annhienktuit.muzikv2.enums.Interpolation import com.annhienktuit.muzikv2.utils.VisualizerHelper import org.apache.commons.math3.analysis.interpolation.AkimaSplineInterpolator import org.apache.commons.math3.analysis.interpolation.LinearInterpolator import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction import kotlin.math.cos import kotlin.math.sin abstract class Painter { private val li = LinearInterpolator() private val sp = AkimaSplineInterpolator() 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 * @param helper the visualizerHelper from VisualizerView */ abstract fun draw(canvas: Canvas, helper: VisualizerHelper,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): FloatArray { val patched = FloatArray(fft.size + 2) fft.forEachIndexed { index, d -> patched[index + 1] = d } 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 } } } } ``` **FFTAudioProcessor** ```java /** * Created by Nhien Nguyen on 5/5/2022 * <p> * An audio processor which forwards the input to the output, * but also takes the input and executes a Fast-Fourier Transformation (FFT) on it. * The results of this transformation is an array of frequencies with their amplitudes, * which will be forwarded to the listener<br> * The sequence diagram is apply in Exoplayer 2.16.1 * <p> * <p> * <center><object style="width: 480px; height: 190px;" type="image/jpeg" * <img * src="https://i.imgur.com/Bjcr9dy.jpg" style="width: 480px; height: 190px" * alt="FFTAudioProcessor Sequence diagram"></object></center> * <p> */ @SuppressWarnings("FieldCanBeLocal") public class FFTAudioProcessor implements AudioProcessor { public static final String TAG = "FFTAudioProcessor"; public static final int UNSUPPORTED_BITRATE_FACTOR = -1; public static final int BITRATE_HIRES_FACTOR = 1; public static final int BITRATE_LOSSY_FACTOR = 3; @BitrateFactor private static int bitrateFactor = BITRATE_HIRES_FACTOR; public static int SAMPLE_SIZE = 4096 * bitrateFactor; //Default 4096 -> The higher SAMPLE_SIZE value, the slower data return public final long EXO_MIN_BUFFER_DURATION_US = 250000L; public final long EXO_MAX_BUFFER_DURATION_US = 750000L; public final long EXO_BUFFER_MULTIPLICATION_FACTOR = 4; private final ExecutorService executorService; public int BUFFER_EXTRA_SIZE = SAMPLE_SIZE * 8; //lossless 8 mp3 2 public FFTAudioProcessor.FFTListener listener; private byte[] mTempByteArray = new byte[SAMPLE_SIZE * 2]; private float[] mSrc = new float[SAMPLE_SIZE]; private float[] mDst = new float[SAMPLE_SIZE + 2]; private float[] fft; private byte[] tempBufferArray; private Noise mNoise; private ByteBuffer mProcessBuffer; private ByteBuffer mFftBuffer; private ByteBuffer mOutputBuffer; private ByteBuffer mSrcBuffer; private int mSrcBufferPosition = 0; private int mAudioTrackBufferSize = 0; private int mChannelCount; private int mSampleRateHz; private int mEncoding = 0; private int mBytesToProcess = SAMPLE_SIZE * 2; private int position; private int limit; private int frameCount; private int singleChannelOutputSize; private int outputSize; private boolean mIsActive; private boolean wasActive; private boolean mInputEnded; private long mFFTProcessTimeMs; private com.annhienktuit.muzikv2.enums.BitrateType mBitrateType = com.annhienktuit.muzikv2.enums.BitrateType.LOSSY; public FFTAudioProcessor() { mProcessBuffer = AudioProcessor.EMPTY_BUFFER; mFftBuffer = AudioProcessor.EMPTY_BUFFER; mOutputBuffer = AudioProcessor.EMPTY_BUFFER; mChannelCount = Format.NO_VALUE; mSampleRateHz = Format.NO_VALUE; executorService = Executors.newSingleThreadExecutor(); } private int getDefaultBufferSizeInBytes() { Log.i(TAG, "getDefaultBufferSizeInBytes"); int outputPcmFrameSize = Util.getPcmFrameSize(mEncoding, mChannelCount); int minBufferSize = AudioTrack.getMinBufferSize(mSampleRateHz, Util.getAudioTrackChannelConfig(mChannelCount), mEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = (int) (minBufferSize * EXO_BUFFER_MULTIPLICATION_FACTOR); int minAppBufferSize = (int) durationUsToFrames(EXO_MIN_BUFFER_DURATION_US) * outputPcmFrameSize; long maxAppBufferSize = Math.max(minBufferSize, (durationUsToFrames(EXO_MAX_BUFFER_DURATION_US) * outputPcmFrameSize)); int bufferSizeInFrames = (int) (Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize) / outputPcmFrameSize); return bufferSizeInFrames * outputPcmFrameSize; } private long durationUsToFrames(long durationUs) { return durationUs * mSampleRateHz / C.MICROS_PER_SECOND; } @Override public boolean configure(int sampleRateHz, int channelCount, int encoding) throws UnhandledFormatException { Log.i(TAG, "Configure"); if (encoding != C.ENCODING_PCM_16BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } mSampleRateHz = sampleRateHz; mChannelCount = channelCount; mEncoding = encoding; wasActive = mIsActive; mIsActive = true; mNoise = Noise.real(SAMPLE_SIZE); mAudioTrackBufferSize = getDefaultBufferSizeInBytes(); mSrcBuffer = ByteBuffer.allocate(mAudioTrackBufferSize + BUFFER_EXTRA_SIZE); Log.i("FFTProcessorDetail", toString()); return !wasActive; } @Override public boolean isActive() { return mIsActive; } @Override public int getOutputChannelCount() { return mChannelCount; } @Override public int getOutputEncoding() { return mEncoding; } @Override public int getOutputSampleRateHz() { return mSampleRateHz; } @Override public void queueInput(ByteBuffer inputBuffer) { Log.i(TAG, "queueInput"); mFFTProcessTimeMs = System.currentTimeMillis(); position = inputBuffer.position(); limit = inputBuffer.limit(); frameCount = (limit - position) / (2 * mChannelCount); singleChannelOutputSize = frameCount * 2; outputSize = frameCount * mChannelCount * 2; if (mProcessBuffer.capacity() < outputSize) { mProcessBuffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); } else { mProcessBuffer.clear(); } if (mFftBuffer.capacity() < singleChannelOutputSize) { mFftBuffer = ByteBuffer.allocateDirect(singleChannelOutputSize).order(ByteOrder.nativeOrder()); } else { mFftBuffer.clear(); } while (position < limit) { int summedUp = 0; for (int channelIndex = 0; channelIndex < mChannelCount; channelIndex++) { short current = inputBuffer.getShort(position + 2 * channelIndex); mProcessBuffer.putShort(current); summedUp += current; } mFftBuffer.putShort((short) (summedUp / mChannelCount)); position += mChannelCount * 2; } inputBuffer.position(limit); // executorService.execute(() -> processFFT(mFftBuffer));\ processFFT(mFftBuffer); mProcessBuffer.flip(); mOutputBuffer = this.mProcessBuffer; } private void processFFT(ByteBuffer buffer) { Log.i(TAG, "processFFT"); if (listener == null) { return; } try { mSrcBuffer.put(buffer.array()); mSrcBufferPosition += buffer.array().length; mBytesToProcess = SAMPLE_SIZE * 2; Byte currentByte = null; while (mSrcBufferPosition > mAudioTrackBufferSize) { mSrcBuffer.position(0); mSrcBuffer.get(mTempByteArray, 0, mBytesToProcess); for (int idx = 0; idx < mTempByteArray.length; idx++) { if (currentByte == null) { currentByte = mTempByteArray[idx]; } else { mSrc[idx / 2] = ((float) currentByte * Byte.MAX_VALUE + mTempByteArray[idx]) / (Byte.MAX_VALUE * Byte.MAX_VALUE); mDst[idx / 2] = 0f; currentByte = null; } } tempBufferArray = new byte[mSrcBuffer.remaining()]; Arrays.fill(tempBufferArray, (byte) 0); mSrcBuffer.get(tempBufferArray); mSrcBuffer.position(mBytesToProcess); mSrcBuffer.compact(); mSrcBufferPosition -= mBytesToProcess; mSrcBuffer.position(mSrcBufferPosition); fft = mNoise.fft(mSrc, mDst); listener.onFFTReady(mSampleRateHz, mChannelCount, fft); Log.i("FFTProcessTime", "Process in " + (System.currentTimeMillis() - mFFTProcessTimeMs) + " ms"); } } catch (Exception e) { e.printStackTrace(); } } @Override public void queueEndOfStream() { mInputEnded = true; mProcessBuffer = AudioProcessor.EMPTY_BUFFER; } @Override public ByteBuffer getOutput() { ByteBuffer outputBuffer = this.mOutputBuffer; this.mOutputBuffer = AudioProcessor.EMPTY_BUFFER; return outputBuffer; } @Override public boolean isEnded() { return mInputEnded && mProcessBuffer == AudioProcessor.EMPTY_BUFFER; } @Override public void flush() { mOutputBuffer = AudioProcessor.EMPTY_BUFFER; mInputEnded = false; } @Override public void reset() { Log.i(TAG, "reset"); flush(); mProcessBuffer = AudioProcessor.EMPTY_BUFFER; mFftBuffer = AudioProcessor.EMPTY_BUFFER; mOutputBuffer = AudioProcessor.EMPTY_BUFFER; mEncoding = Format.NO_VALUE; mChannelCount = Format.NO_VALUE; mSampleRateHz = Format.NO_VALUE; // mNoise.close(); } public void setBitrateType(int bitrate) { BitrateType prevType = mBitrateType; if (bitrate > 400000) mBitrateType = BitrateType.HIRES; else mBitrateType = BitrateType.LOSSY; if (mBitrateType != prevType) { config(mBitrateType); } } private void config(BitrateType bitrateType) { if (bitrateType == BitrateType.HIRES) { setupForHiRes(); } else { setupForLossy(); } } private void setupForLossy() { SAMPLE_SIZE = 4096 * BITRATE_LOSSY_FACTOR; mTempByteArray = null; mSrc = null; mDst = null; mTempByteArray = new byte[SAMPLE_SIZE * 2]; mSrc = new float[SAMPLE_SIZE]; mDst = new float[SAMPLE_SIZE + 2]; BUFFER_EXTRA_SIZE = SAMPLE_SIZE * 8; mBytesToProcess = SAMPLE_SIZE * 2; listener.onBitrateChange(BITRATE_LOSSY_FACTOR); } private void setupForHiRes() { SAMPLE_SIZE = 4096 * BITRATE_HIRES_FACTOR; mTempByteArray = null; mSrc = null; mDst = null; mTempByteArray = new byte[SAMPLE_SIZE * 2]; mSrc = new float[SAMPLE_SIZE]; mDst = new float[SAMPLE_SIZE + 2]; BUFFER_EXTRA_SIZE = SAMPLE_SIZE * 8; mBytesToProcess = SAMPLE_SIZE * 2; listener.onBitrateChange(BITRATE_HIRES_FACTOR); } public void closeInstance() { mNoise.close(); } public void showToast(Context context, String msg) { Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } public int getSampleSize() { return SAMPLE_SIZE; } @NonNull @Override public String toString() { return "FFTAudioProcessor{" + "SAMPLE_SIZE=" + SAMPLE_SIZE + ", BUFFER_EXTRA_SIZE=" + BUFFER_EXTRA_SIZE + ", mAudioTrackBufferSize=" + mAudioTrackBufferSize + ", mChannelCount=" + mChannelCount + ", mSampleRateHz=" + mSampleRateHz + ", mEncoding=" + mEncoding + ", bitrateFactor=" + bitrateFactor + '}'; } @Retention(RetentionPolicy.SOURCE) @IntDef({BITRATE_HIRES_FACTOR, BITRATE_LOSSY_FACTOR, UNSUPPORTED_BITRATE_FACTOR}) public @interface BitrateFactor { } public interface FFTListener { void onFFTReady(int sampleRateHz, int channelCount, float[] FFTArray); void onBitrateChange(@BitrateFactor int bitrateFactor); } } ``` **visualizer_layout.xml** ```xml <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" tools:parentTag="com.annhienktuit.muzikv2.ui.views.visualizer.CircularVisualizerLayout" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.annhienktuit.muzikv2.ui.views.visualizer.nextgen.FFTVisualizer android:id="@+id/fftVisualizerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintStart_toStartOf="@+id/discArtworkView" app:layout_constraintTop_toTopOf="parent" /> <com.annhienktuit.muzikv2.ui.views.visualizer.DiscArtworkView android:id="@+id/discArtworkView" android:layout_width="275dp" android:layout_height="275dp" app:layout_constraintBottom_toBottomOf="@+id/fftVisualizerView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> ```