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.