---
# System prepended metadata

title: BUILD 14/9

---

# BUILD 14/9

**Hope this build will resolve the fucking cannot play(0,-1,null) problem 😢**

**FFTAudioProcessor.java**
```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 int SAMPLE_SIZE = 4096; //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 boolean isDebugging = false;
    public int BUFFER_EXTRA_SIZE = SAMPLE_SIZE * 8; 
    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; //UNUSED YET
    private int outputSize;
    private boolean mIsActive;
    private boolean wasActive;
    private boolean mInputEnded;
    private long mFFTProcessTimeMs;

    public FFTAudioProcessor() {
        mProcessBuffer = AudioProcessor.EMPTY_BUFFER;
        mFftBuffer = AudioProcessor.EMPTY_BUFFER;
        mOutputBuffer = AudioProcessor.EMPTY_BUFFER;
        mChannelCount = Format.NO_VALUE;
        mSampleRateHz = Format.NO_VALUE;
    }

    private int getDefaultBufferSizeInBytes() {
        if (isDebugging) 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 {
        if (isDebugging) 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) {
        if (isDebugging) Log.i(TAG, "queueInput");
        int remaining = inputBuffer.remaining();
        if (remaining == 0) return;
        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() < remaining) {
            mFftBuffer = ByteBuffer.allocateDirect(remaining).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);
                try {
                    mProcessBuffer.putShort(current);
                } catch (BufferOverflowException e) {
                    Log.e(TAG, e.getLocalizedMessage());
                }
                summedUp += current;
            }
            try {
                mFftBuffer.putShort((short) (summedUp / mChannelCount));
            } catch (BufferOverflowException e) {
                Log.e(TAG, e.getLocalizedMessage());
            }
            position += mChannelCount * 2;
        }
        inputBuffer.position(limit);
        processFFT(mFftBuffer);
        mProcessBuffer.flip();
        mOutputBuffer = this.mProcessBuffer;
    }

    private void processFFT(ByteBuffer buffer) {
        if (isDebugging) 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);
                if (isDebugging)
                    Log.i("FFTProcessTime", "Process in " + (System.currentTimeMillis() - mFFTProcessTimeMs) + " ms");
                mFFTProcessTimeMs = System.currentTimeMillis();
            }
        } 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() {
        if (isDebugging) 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;
    }

    public void closeInstance() {
        mNoise.close();
    }

    public void showToast(Context context, String msg) {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
    }

    @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 +
                '}';
    }

    public interface FFTListener {
        void onFFTReady(int sampleRateHz, int channelCount, float[] FFTArray);
    }
}

```

**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),
    private var startHz: Int = 20,
    private var endHz: Int = 2000,
    private var interpolator: Interpolation = Interpolation.SPLINE,
    var discMargin: Int = 0
) : Painter() {
    private var gravityPoints = Array(0) { GravityModel(0f) }
    private val barWidth =
        context.resources.getDimensionPixelSize(R.dimen.default_visualizer_bar_width).toFloat()
    private val debugPaint = Paint()
    private val radiusR: Float = .4f
    private val ampR: Float = 1f //The rate of amplification
    private val angleOffset = 2 * PI.toFloat() / numberOfBar
    private var shortest = 0F
    var discRadius = 0
        set(value) {
            field = value
            shortest = getShortest()
            adjustRadius(shortest)
        }
    var firstTimeRunning = true
    var isPlaying = false
    var visualizerMaxHeight = 0
    var curveUtil: PolynomialSplineFunction? = null
    lateinit var fft: FloatArray

    // 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()

    // Fields for draw
    private var start = Pair(0F, 0F)
    private var stop = Pair(0F, 0F)
    private var currentHeight: Int = 0
    private var currentWidth: Int = 0

    init {
        paint.apply {
            color = Color.WHITE
            style = Paint.Style.FILL
            strokeWidth = barWidth
            strokeCap = Paint.Cap.ROUND
        }
        debugPaint.apply {
            isAntiAlias = true
            color = Color.RED
            style = Paint.Style.FILL
            strokeWidth = barWidth
        }
    }

    override fun calc(helper: VisualizerHelper) {
        fft = helper.getFftMagnitudeRange(startHz, endHz)
        fft = getPowerFft(fft)
        fft = if (firstTimeRunning) {
            getCircleFft(fft, true)
        } else {
            getCircleFft(fft)
        }
        minMaxNormalize(fft, visualizerMaxHeight, 0)
        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)
    }

    /**
     * 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)
                }
            }
        }
    }

    /**
     * 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
        }
    }

    /**
     * Handle high frequency value to avoid low variability
     * If that data is in range [[visualizerMaxHeight] - [highFreqDelta] , [visualizerMaxHeight]]
     */
    private fun handleHighFreqValue(idx: Int) {
        if (frequencyMap.containsKey(idx)) {
            frequencyMap[idx] = frequencyMap[idx]!!.plus(1)
        } else {
            frequencyMap[idx] = 1
        }
        if (frequencyMap[idx]!! > 3) {
            frequencyMap[idx] = 0
            fft[idx] =
                (random.nextInt((visualizerMaxHeight - highFreqDelta) - highFreqDelta + 1) + highFreqDelta).toFloat()
        }
    }

    override fun draw(canvas: Canvas, width: Int, height: Int, skipCalc: Boolean) {
        if (skipCalc) return
        currentWidth = width
        currentHeight = height
        if (firstTimeRunning) {
            shortest = min(width, height).toFloat()
            adjustRadius(shortest)
        }
        drawHelperCircleLine(canvas, .5f, .5f) {
            if (!firstTimeRunning) {
                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
                }
            }
        }
        canvas.drawLines(points, paint)
        if (firstTimeRunning) firstTimeRunning = false
    }

    /**
     * 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 * (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
        }
    }

    /**
     * @return if number is in range [[start], [stop]]
     */
    private fun Float.isExclusive(start: Int, stop: Int): Boolean {
        return (this >= start && this <= stop)
    }

    private fun FloatArray.contentToStringWithIndex(): String {
        val strBuilder = StringBuilder()
        strBuilder.append("[")
        for (index in 0 until this.size) {
            strBuilder.append(" $index: ${this[index]},")
        }
        strBuilder.append("]")
        return strBuilder.toString()
    }

    private fun getShortest(): Float = min(currentWidth, currentHeight).toFloat()

    companion object {
        private const val numberOfBar: Int = 100
        val points = FloatArray(4 * numberOfBar)
    }
}


```
**Painter.kt**
```kotlin
abstract class Painter {

    abstract var paint: Paint
    private val li = LinearInterpolator()
    private val sp = AkimaSplineInterpolator()
    private val quietThreshold = 5f
    private var patched: FloatArray = FloatArray(0)
    private var psf: PolynomialSplineFunction? = null
    private var nRaw: Int = 0
    private var xRaw = DoubleArray(0)
    private var yRaw = DoubleArray(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: String
    ): 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) {
            "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? {
        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
    }

    /**
     * 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()
    }

    /**
     * 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()
    }

    open fun saveCurrentState() {}

    /**
     * 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]"
        }
    }
}
```


