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