Refactoring AudioUtils

- Fixes a bug with the processing taking to long
- Added a bunch of comments, to make maintaining this more easier
- Added LifeCycle Awareness

Signed-off-by: Julius Linus <julius.linus@nextcloud.com>
This commit is contained in:
Julius Linus 2023-10-12 10:55:47 -05:00 committed by Marcel Hibbe
parent d3f55b0f33
commit 79b366c5ba
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
2 changed files with 87 additions and 11 deletions

View File

@ -193,7 +193,7 @@ import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.AudioUtils.audioFileToFloatArray import com.nextcloud.talk.utils.AudioUtils
import com.nextcloud.talk.utils.ContactUtils import com.nextcloud.talk.utils.ContactUtils
import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateConstants
@ -507,6 +507,7 @@ class ChatActivity :
val text = getString(roomToken, "") val text = getString(roomToken, "")
binding.messageInputView.messageInput.setText(text) binding.messageInputView.messageInput.setText(text)
} }
this.lifecycle.addObserver(AudioUtils)
} }
override fun onStop() { override fun onStop() {
@ -530,6 +531,7 @@ class ChatActivity :
apply() apply()
} }
} }
this.lifecycle.removeObserver(AudioUtils)
} }
@Suppress("LongMethod") @Suppress("LongMethod")
@ -909,7 +911,7 @@ class ChatActivity :
message.isDownloadingVoiceMessage = true message.isDownloadingVoiceMessage = true
adapter?.update(message) adapter?.update(message)
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
val r = audioFileToFloatArray(file) val r = AudioUtils.audioFileToFloatArray(file)
message.voiceMessageFloatArray = r message.voiceMessageFloatArray = r
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
startPlayback(message) startPlayback(message)

View File

@ -27,6 +27,8 @@ import android.media.MediaExtractor
import android.media.MediaFormat import android.media.MediaFormat
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.ByteOrder import java.nio.ByteOrder
@ -38,11 +40,26 @@ import kotlin.math.abs
* AudioUtils are for processing raw audio using android's low level APIs, for more information read here * AudioUtils are for processing raw audio using android's low level APIs, for more information read here
* [MediaCodec documentation](https://developer.android.com/reference/android/media/MediaCodec) * [MediaCodec documentation](https://developer.android.com/reference/android/media/MediaCodec)
*/ */
object AudioUtils { object AudioUtils : DefaultLifecycleObserver {
private val TAG = AudioUtils::class.java.simpleName private val TAG = AudioUtils::class.java.simpleName
private const val VALUE_10 = 10 private const val VALUE_10 = 10
private const val TIME_LIMIT = 5000 private const val TIME_LIMIT = 5000
private const val DEFAULT_SIZE = 500 private const val DEFAULT_SIZE = 500
private enum class LifeCycleFlag {
PAUSED,
RESUMED
}
private lateinit var currentLifeCycleFlag: LifeCycleFlag
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
currentLifeCycleFlag = LifeCycleFlag.RESUMED
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
currentLifeCycleFlag = LifeCycleFlag.PAUSED
}
/** /**
* Suspension function, returns a FloatArray of size 500, containing the values of an audio file squeezed between * Suspension function, returns a FloatArray of size 500, containing the values of an audio file squeezed between
@ -51,25 +68,55 @@ object AudioUtils {
@Throws(IOException::class) @Throws(IOException::class)
suspend fun audioFileToFloatArray(file: File): FloatArray { suspend fun audioFileToFloatArray(file: File): FloatArray {
return suspendCoroutine { return suspendCoroutine {
// Used to keep track of the time it took to process the audio file
val startTime = SystemClock.elapsedRealtime() val startTime = SystemClock.elapsedRealtime()
var result = mutableListOf<Float>()
// Always a FloatArray of Size 500
var result: MutableList<Float>? = mutableListOf()
// Setting the file path to the audio file
val path = file.path val path = file.path
val mediaExtractor = MediaExtractor() val mediaExtractor = MediaExtractor()
mediaExtractor.setDataSource(path) mediaExtractor.setDataSource(path)
// Basically just boilerplate to set up meta data for the audio file
val mediaFormat = mediaExtractor.getTrackFormat(0) val mediaFormat = mediaExtractor.getTrackFormat(0)
// Frame rate is required for encoders, optional for decoders. So we set it to null here.
mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null) mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null)
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 0) mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 0)
mediaExtractor.release() mediaExtractor.release()
// More Boiler plate to set up the codec
val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS) val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)
val codecName = mediaCodecList.findDecoderForFormat(mediaFormat) val codecName = mediaCodecList.findDecoderForFormat(mediaFormat)
val mediaCodec = MediaCodec.createByCodecName(codecName) val mediaCodec = MediaCodec.createByCodecName(codecName)
/**
************************************ Media Codec *******************************************
*
* INPUT BUFFERS OUTPUT BUFFERS
*
*
* Empty Buffer Filled Buffer
* [][][] [-][-][-]
*
* Client Codec Client
*
*
* [-][-][-] [][][]
* Filled BufferEmpty Buffer
*
* Client provides Client consumes
* input Data output data
*
********************************************************************************************
*/
mediaCodec.setCallback(object : MediaCodec.Callback() { mediaCodec.setCallback(object : MediaCodec.Callback() {
private var extractor: MediaExtractor? = null private var extractor: MediaExtractor? = null
val tempList = mutableListOf<Float>() val tempList = mutableListOf<Float>()
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
// Setting up the extractor if not already done
if (extractor == null) { if (extractor == null) {
extractor = MediaExtractor() extractor = MediaExtractor()
try { try {
@ -79,6 +126,8 @@ object AudioUtils {
e.printStackTrace() e.printStackTrace()
} }
} }
// Boiler plate, Extracts a buffer of encoded audio data to be sent to the codec for processing
val byteBuffer = codec.getInputBuffer(index) val byteBuffer = codec.getInputBuffer(index)
if (byteBuffer != null) { if (byteBuffer != null) {
val sampleSize = extractor!!.readSampleData(byteBuffer, 0) val sampleSize = extractor!!.readSampleData(byteBuffer, 0)
@ -96,6 +145,7 @@ object AudioUtils {
} }
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
// Boiler plate to get the audio data in a usable form
val outputBuffer = codec.getOutputBuffer(index) val outputBuffer = codec.getOutputBuffer(index)
val bufferFormat = codec.getOutputFormat(index) val bufferFormat = codec.getOutputFormat(index)
val samples = outputBuffer!!.order(ByteOrder.nativeOrder()).asShortBuffer() val samples = outputBuffer!!.order(ByteOrder.nativeOrder()).asShortBuffer()
@ -104,23 +154,37 @@ object AudioUtils {
return return
} }
val sampleLength = (samples.remaining() / numChannels) val sampleLength = (samples.remaining() / numChannels)
// Squeezes the value of each sample between [0,1) using y = (x-1)/x // Squeezes the value of each sample between [0,1) using y = (x-1)/x
for (i in 0 until sampleLength) { for (i in 0 until sampleLength) {
val x = abs(samples[i * numChannels + index].toInt()) / VALUE_10 val x = abs(samples[i * numChannels + index].toInt()) / VALUE_10
val y = (if (x > 0) ((x - 1) / x.toFloat()) else x.toFloat()) val y = (if (x > 0) ((x - 1) / x.toFloat()) else x.toFloat())
tempList.add(y) tempList.add(y)
} }
codec.releaseOutputBuffer(index, false) codec.releaseOutputBuffer(index, false)
// Cancels the process if it ends, exceeds the time limit, or the activity falls out of view
val currTime = SystemClock.elapsedRealtime() - startTime val currTime = SystemClock.elapsedRealtime() - startTime
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM > 0 || currTime > TIME_LIMIT) { if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM > 0 ||
currTime > TIME_LIMIT ||
currentLifeCycleFlag == LifeCycleFlag.PAUSED
) {
Log.d(
TAG,
"Processing ended with time: $currTime \n" +
"Is finished: ${info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM > 0} \n" +
"Lifecycle state: $currentLifeCycleFlag"
)
codec.stop() codec.stop()
codec.release() codec.release()
extractor!!.release() extractor!!.release()
extractor = null extractor = null
if (currTime < TIME_LIMIT) { result = if (currTime < TIME_LIMIT) {
result = tempList tempList
} else { } else {
Log.d(TAG, "time limit exceeded") Log.e(TAG, "Error in MediaCodec Callback:\n\tonOutputBufferAvailable: Time limit exceeded")
null
} }
} }
} }
@ -131,20 +195,30 @@ object AudioUtils {
codec.release() codec.release()
extractor!!.release() extractor!!.release()
extractor = null extractor = null
result = tempList result = null
} }
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
// unused atm // unused atm
} }
}) })
// More Boiler plate to start the codec
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT) mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
mediaCodec.configure(mediaFormat, null, null, 0) mediaCodec.configure(mediaFormat, null, null, 0)
mediaCodec.start() mediaCodec.start()
while (result.size <= 0) {
// This runs until the codec finishes or the time limit is exceeded, or an error occurs
// If the time limit is exceed or an error occurs, the result should be null
while (result != null && result!!.size <= 0) {
continue continue
} }
it.resume(shrinkFloatArray(result.toFloatArray(), DEFAULT_SIZE))
if (result != null && result!!.size > DEFAULT_SIZE) {
it.resume(shrinkFloatArray(result!!.toFloatArray(), DEFAULT_SIZE))
} else {
it.resume(FloatArray(DEFAULT_SIZE))
}
} }
} }