What are some correct settings to encode an audio byte stream (from mic) into AAC in an android app

91 Views Asked by At

My son is trying to write an android app (in kotlin) that reads the microphone and saves the result into an AAC file. He succeeded for a single piece of sound recorded in one shot. Now he needs to concatenate some sounds. To do that he needs to cope with stream buffers. His program can apparently catch the sound and saves an AAC file but this file is not in a correct format. In his last try, when the PCM file is played, the speed of the sound is twice the original speed. We are trying to see if it's not due to bad settings in the headers of the AAC file but we are not encoding specialists at all, so any help to find the correct values for the AAC headers would be appreciated! Note that his code is essentially based on this one : Android AudioRecord example and the code for the saveByteArrayAsAAC and saveRecordingAsAAC functions was written with the help of ChatGPT... My son also noticed that when he records a few seconds the file size seems to be coherent (about 30ko), but if the recording time is larger, then the output file size drops to 16 bytes only... Thanks by advance, Eric

package com.example.testcpp

import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.MediaPlayer
import android.media.MediaRecorder
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException

class MainActivity : AppCompatActivity() {
    private val RECORDER_SAMPLERATE = 44100 //8000
    private val RECORDER_CHANNELS = AudioFormat.CHANNEL_IN_MONO
    private val RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT
    private var recorder: AudioRecord? = null
    private var recordingThread: Thread? = null
    private var isRecording = false

    @RequiresApi(Build.VERSION_CODES.S)
    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        var REC = findViewById<Button>(R.id.REC)
        var STOP = findViewById<Button>(R.id.STOP)

        REC.isEnabled = true
        STOP.isEnabled = false

        val bufferSize = AudioRecord.getMinBufferSize(
            RECORDER_SAMPLERATE,
            RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING
        )

        var BufferElements2Rec = 1024 // want to play 2048 (2K) since 2 bytes we use only 1024

        var BytesPerElement = 2 // 2 bytes in 16bit format

fun addADTStoPacket(packet: ByteArray, packetLen: Int): ByteArray {
    val profile =  MediaCodecInfo.CodecProfileLevel.AACObjectLC // AAC LC 
    val freqIdx = 4 // Sampling frequency index: 44.1KHz
    val chanCfg = 1 // Number of channels: Mono

    val packetLenWithHeader = packetLen + 7 // ADTS header is 7 bytes

    val adts = ByteArray(7)

    adts[0] = 0xFF.toByte()
    adts[1] = 0xF9.toByte()
    adts[2] = (((profile - 1) shl 6) + (freqIdx shl 2) + (chanCfg shr 2)).toByte()
    adts[3] = (((chanCfg and 3) shl 6) + (packetLenWithHeader shr 11)).toByte()
    adts[4] = (((packetLenWithHeader and 0x7FF) shr 3)).toByte()
    adts[5] = (((packetLenWithHeader and 7) shl 5) + 0x1F).toByte()
    adts[6] = 0xFC.toByte()

    val result = ByteArray(packetLenWithHeader + 7)
    System.arraycopy(adts, 0, result, 0, 7)
    System.arraycopy(packet, 0, result, 7, packetLen)

    return result
}

 



fun encodeToAAC(inputData: ByteArray, inputSize: Int): ByteArray? {

    try {
        val mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, RECORDER_SAMPLERATE, 1)
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 705600)//64000
        mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)

        val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
        codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        codec.start()

        val outputStream = ByteArrayOutputStream()

        val inputBuffers = codec.inputBuffers
        val outputBuffers = codec.outputBuffers

        var inputOffset = 0
        val presentationTimeUs: Long = 0

        while (true) {
            val inputBufferIndex = codec.dequeueInputBuffer(-1)
            if (inputBufferIndex >= 0) {
                val inputBuffer = inputBuffers[inputBufferIndex]
                inputBuffer.clear()

                val chunkSize = kotlin.math.min(inputData.size - inputOffset, inputBuffer.remaining())
                if (chunkSize <= 0) {
                    codec.queueInputBuffer(inputBufferIndex, 0, 0, presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                    break
                }

                inputBuffer.put(inputData, inputOffset, chunkSize)
                inputOffset += chunkSize

                codec.queueInputBuffer(inputBufferIndex, 0, chunkSize, presentationTimeUs, 0)
            }

            val bufferInfo = MediaCodec.BufferInfo()
            val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 1000)
            when {
                outputBufferIndex >= 0 -> {
                    val outputBuffer = outputBuffers[outputBufferIndex]
                    val chunk = ByteArray(bufferInfo.size)
                    outputBuffer.get(chunk)

                    outputStream.write(chunk, 0, bufferInfo.size)
                    codec.releaseOutputBuffer(outputBufferIndex, false)

                    if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        break
                    }
                }
                outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    // Ignoring it for now
                }
                else -> break
            }
        }

        codec.stop()
        codec.release()

        val outputData = outputStream.toByteArray()
        // Add ADTS headers to the encoded AAC data
        val aacDataWithHeaders = addADTStoPacket(outputData, outputData.size)

        return aacDataWithHeaders
    } catch (e: Exception) {
        e.printStackTrace()
        return null
    }


        // Function to save AAC data to a file
        fun saveByteArrayAsAAC(data: ByteArray, outputFilePath: String) {
            println("saveByteArrayAsAAC starts")
            val outputStream = FileOutputStream(outputFilePath)
            val bufferedOutputStream = BufferedOutputStream(outputStream)
            println("saveByteArrayAsAAC")
            // Write the encoded AAC data to the output file stream
            bufferedOutputStream.write(data)
            bufferedOutputStream.flush()
            bufferedOutputStream.close()
            println("saveByteArrayAsAAC end")
        }

        // Inside the recording loop or after recording is complete
        fun saveRecordingAsAAC(audioData: ByteArray, length: Int) {
            // Perform audio recording and obtain raw PCM audio data in audioData array

            // Convert raw PCM audio data to AAC format
            val encodedData = encodeToAAC(audioData, length)
            var outputFilePath: String? = null
            outputFilePath = externalCacheDir!!.absolutePath
            outputFilePath += "/audiorecordtestteste_ultime.aac"
            if (encodedData != null) {
                saveByteArrayAsAAC(encodedData, outputFilePath)
            }
            
        }

        fun short2byte(sData: ShortArray): ByteArray {
            val shortArrsize = sData.size
            val bytes = ByteArray(shortArrsize * 2)
            for (i in 0 until shortArrsize) {
                bytes[i * 2] = (sData[i].toInt() and 0x00FF).toByte()
                bytes[i * 2 + 1] = (sData[i].toInt() shr 8).toByte()
                sData[i] = 0
            }
            return bytes
        }


        fun writeAudioDataToFile() {
            // Write the output audio in byte
            var filePath: String? = null
            filePath = externalCacheDir!!.absolutePath
            filePath += "/recording.pcm"
            println(filePath)
            val sData = ShortArray(BufferElements2Rec)
            var os: FileOutputStream? = null
            var intermediary  : ByteArrayOutputStream = ByteArrayOutputStream()
            try {
                os = FileOutputStream(filePath)
            } catch (e: FileNotFoundException) {
                e.printStackTrace()
            }
            while (isRecording) {
                // gets the voice output from microphone to byte format
                recorder!!.read(sData, 0, BufferElements2Rec)
                println("Short writing to file$sData")
                try {
                    // // writes the data to file from buffer
                    // // stores the voice buffer
                    val bData = short2byte(sData)
                    
                    println("bData size : " + bData.size)
                    os!!.write(bData, 0, BufferElements2Rec * BytesPerElement)
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
           saveRecordingAsAAC(intermediary.toByteArray(), intermediary.size())
            try {
                os!!.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }


        fun startRecording() {
            if(checkPermission()) {
                recorder = AudioRecord(
                    MediaRecorder.AudioSource.MIC,
                    RECORDER_SAMPLERATE, RECORDER_CHANNELS,
                    RECORDER_AUDIO_ENCODING, BufferElements2Rec * BytesPerElement
                )

                recorder!!.startRecording()
                isRecording = true
                recordingThread = Thread({ writeAudioDataToFile() }, "AudioRecorder Thread")
                recordingThread!!.start()

            }else{ActivityCompat.requestPermissions(
                this@MainActivity, arrayOf(
                    android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.WRITE_EXTERNAL_STORAGE
                ), 1)
            }
        }


        fun stopRecording() {


            if (null != recorder) {
                isRecording = false
                recorder!!.stop()
                recorder!!.release()
                recorder = null
                recordingThread = null
            }
        }

        REC.setOnClickListener(View.OnClickListener {
            REC.isEnabled = false
            STOP.isEnabled = true
            startRecording()

        })

        STOP.setOnClickListener(View.OnClickListener {
            REC.isEnabled = true
            STOP.isEnabled = false
             stopRecording()
        })


    }


    private fun checkPermission(): Boolean {
        val first = ActivityCompat.checkSelfPermission(
            applicationContext,
            android.Manifest.permission.RECORD_AUDIO
        )
        return first == PackageManager.PERMISSION_GRANTED
    }

}


With the above settings on the AAC headers we could get an AAC file that cannot be opened in the player, but we noticed at the PCM level that the recorded voice is played with a wrong speed. When we try to open the AAC file the player starts but the cursor instantaneously moves to the end of the recorded track.

0

There are 0 best solutions below