Coverage Summary for Class: PrimitiveAudioExtractor (com.vsevolodganin.clicktrack.primitiveaudio)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
PrimitiveAudioExtractor |
0%
(0/4)
|
0%
(0/53)
|
0%
(0/69)
|
0%
(0/360)
|
PrimitiveAudioExtractor$Companion |
|
Total |
0%
(0/4)
|
0%
(0/53)
|
0%
(0/69)
|
0%
(0/360)
|
package com.vsevolodganin.clicktrack.primitiveaudio
import android.content.res.AssetFileDescriptor
import android.media.AudioFormat
import android.media.MediaCodec
import android.media.MediaCodecList
import android.media.MediaExtractor
import android.media.MediaFormat
import com.vsevolodganin.clicktrack.di.component.ApplicationScope
import com.vsevolodganin.clicktrack.utils.log.Logger
import com.vsevolodganin.clicktrack.utils.media.bytesPerSecond
import com.vsevolodganin.clicktrack.utils.media.channelCount
import com.vsevolodganin.clicktrack.utils.media.pcmEncoding
import com.vsevolodganin.clicktrack.utils.media.sampleRate
import me.tatarka.inject.annotations.Inject
import java.nio.ByteBuffer
import kotlin.math.min
@ApplicationScope
@Inject
class PrimitiveAudioExtractor(
private val logger: Logger,
) {
fun extract(afd: AssetFileDescriptor, maxSeconds: Int): PrimitiveAudioData? {
val mediaExtractor = MediaExtractor()
try {
mediaExtractor.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
for (trackIndex in 0 until mediaExtractor.trackCount) {
val trackFormat = mediaExtractor.getTrackFormat(trackIndex)
val trackMime = trackFormat.getString(MediaFormat.KEY_MIME) ?: continue
if (trackMime.startsWith("audio/")) {
return extractPcm(mediaExtractor, trackIndex, trackFormat, maxSeconds)
}
}
} catch (t: Throwable) {
logger.logError(TAG, "Failed to extract PCM", t)
} finally {
mediaExtractor.release()
}
return null
}
private fun extractPcm(
mediaExtractor: MediaExtractor,
trackIndex: Int,
trackFormat: MediaFormat,
maxSeconds: Int,
): PrimitiveAudioData? {
var codec: MediaCodec? = null
return try {
val codecName = MediaCodecList(MediaCodecList.REGULAR_CODECS).findDecoderForFormat(trackFormat)
codec = MediaCodec.createByCodecName(codecName).apply {
configure(trackFormat, null, null, 0)
start()
}
val outputTrackFormat = codec.outputFormat
val maxBytes = maxSeconds * outputTrackFormat.bytesPerSecond()
val resultByteBuffer = ByteBuffer.allocateDirect(maxBytes)
var endOfInput = false
var endOfOutput = false
mediaExtractor.selectTrack(trackIndex)
while (!endOfInput || !endOfOutput) {
if (!endOfInput) {
val inputBufferIndex = codec.dequeueInputBuffer(0L)
if (inputBufferIndex >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferIndex)!!
var sampleSize = mediaExtractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
sampleSize = 0
endOfInput = true
}
codec.queueInputBuffer(
inputBufferIndex,
0,
sampleSize,
mediaExtractor.sampleTime,
if (endOfInput) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0,
)
mediaExtractor.advance()
}
}
if (!endOfOutput) {
val bufferInfo = MediaCodec.BufferInfo()
val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0L)
if (outputBufferIndex >= 0) {
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 ||
resultByteBuffer.remaining() == 0
) {
endOfOutput = true
}
val sizeToWrite = min(resultByteBuffer.remaining(), bufferInfo.size)
val outputBuffer = codec.getOutputBuffer(outputBufferIndex)!!
repeat(sizeToWrite) {
resultByteBuffer.put(outputBuffer.get())
}
codec.releaseOutputBuffer(outputBufferIndex, false)
}
}
}
PrimitiveAudioData(
bytes = resultByteBuffer.array().copyOf(resultByteBuffer.position()),
encoding = audioFormatEncodingToCommon(outputTrackFormat.pcmEncoding()) ?: return null,
sampleRate = outputTrackFormat.sampleRate(),
channelCount = outputTrackFormat.channelCount(),
)
} finally {
try {
codec?.stop()
} finally {
codec?.release()
}
}
}
private fun audioFormatEncodingToCommon(encoding: Int): PrimitiveAudioData.Encoding? = when (encoding) {
AudioFormat.ENCODING_PCM_8BIT -> PrimitiveAudioData.Encoding.PCM_UNSIGNED_8BIT
AudioFormat.ENCODING_PCM_16BIT -> PrimitiveAudioData.Encoding.PCM_SIGNED_16BIT_LITTLE_ENDIAN
AudioFormat.ENCODING_PCM_24BIT_PACKED -> PrimitiveAudioData.Encoding.PCM_SIGNED_24BIT_LITTLE_ENDIAN
AudioFormat.ENCODING_PCM_32BIT -> PrimitiveAudioData.Encoding.PCM_SIGNED_32BIT_LITTLE_ENDIAN
AudioFormat.ENCODING_PCM_FLOAT -> PrimitiveAudioData.Encoding.PCM_FLOAT_32BIT_LITTLE_ENDIAN
else -> {
logger.logError(TAG, "Unknown PCM encoding: $encoding")
null
}
}
private companion object {
const val TAG = "PrimitiveAudioExtractor"
}
}