mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
Merge pull request #1678 from davotoula/sonar-fixes
Sonar fixes - reduce Cognitive Complexity in VoiceAnonymizer.kt
This commit is contained in:
+269
-162
@@ -136,6 +136,116 @@ class VoiceAnonymizer {
|
||||
val duration: Int,
|
||||
)
|
||||
|
||||
private data class AudioTrackInfo(
|
||||
val trackIndex: Int,
|
||||
val format: MediaFormat,
|
||||
val mime: String,
|
||||
val sampleRate: Int,
|
||||
val durationUs: Long,
|
||||
)
|
||||
|
||||
private fun findAudioTrack(extractor: MediaExtractor): AudioTrackInfo {
|
||||
for (i in 0 until extractor.trackCount) {
|
||||
val trackFormat = extractor.getTrackFormat(i)
|
||||
val mime = trackFormat.getString(MediaFormat.KEY_MIME)
|
||||
if (mime?.startsWith("audio/") == true) {
|
||||
return AudioTrackInfo(
|
||||
trackIndex = i,
|
||||
format = trackFormat,
|
||||
mime = mime,
|
||||
sampleRate = trackFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),
|
||||
durationUs = trackFormat.getLong(MediaFormat.KEY_DURATION),
|
||||
)
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("No audio track found in file")
|
||||
}
|
||||
|
||||
private class DecoderInputFeeder(
|
||||
private val decoder: MediaCodec,
|
||||
private val extractor: MediaExtractor,
|
||||
private val durationUs: Long,
|
||||
private val onProgress: (Float) -> Unit,
|
||||
) {
|
||||
var isDone = false
|
||||
private set
|
||||
|
||||
fun feedInput() {
|
||||
if (isDone) return
|
||||
|
||||
val inputBufferIndex = decoder.dequeueInputBuffer(10000)
|
||||
if (inputBufferIndex < 0) return
|
||||
|
||||
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
|
||||
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
||||
|
||||
if (sampleSize < 0) {
|
||||
queueEndOfStream(inputBufferIndex)
|
||||
} else {
|
||||
queueSampleData(inputBufferIndex, sampleSize)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueEndOfStream(inputBufferIndex: Int) {
|
||||
decoder.queueInputBuffer(
|
||||
inputBufferIndex,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
MediaCodec.BUFFER_FLAG_END_OF_STREAM,
|
||||
)
|
||||
isDone = true
|
||||
}
|
||||
|
||||
private fun queueSampleData(
|
||||
inputBufferIndex: Int,
|
||||
sampleSize: Int,
|
||||
) {
|
||||
val presentationTimeUs = extractor.sampleTime
|
||||
decoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0)
|
||||
extractor.advance()
|
||||
reportProgress(presentationTimeUs)
|
||||
}
|
||||
|
||||
private fun reportProgress(presentationTimeUs: Long) {
|
||||
if (durationUs > 0) {
|
||||
onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DecoderOutputDrainer(
|
||||
private val decoder: MediaCodec,
|
||||
private val pcmSamples: MutableList<Float>,
|
||||
) {
|
||||
private val bufferInfo = MediaCodec.BufferInfo()
|
||||
var isDone = false
|
||||
private set
|
||||
|
||||
fun drainOutput() {
|
||||
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000)
|
||||
if (outputBufferIndex < 0) return
|
||||
|
||||
extractPcmSamples(outputBufferIndex)
|
||||
decoder.releaseOutputBuffer(outputBufferIndex, false)
|
||||
checkForEndOfStream()
|
||||
}
|
||||
|
||||
private fun extractPcmSamples(outputBufferIndex: Int) {
|
||||
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
|
||||
val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer()
|
||||
while (shortBuffer.hasRemaining()) {
|
||||
pcmSamples.add(shortBuffer.get() / 32768f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForEndOfStream() {
|
||||
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
|
||||
isDone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decodeAudioToPcm(
|
||||
inputFile: File,
|
||||
onProgress: (Float) -> Unit,
|
||||
@@ -145,95 +255,43 @@ class VoiceAnonymizer {
|
||||
|
||||
try {
|
||||
extractor.setDataSource(inputFile.absolutePath)
|
||||
val trackInfo = findAudioTrack(extractor)
|
||||
extractor.selectTrack(trackInfo.trackIndex)
|
||||
|
||||
var audioTrackIndex = -1
|
||||
var format: MediaFormat? = null
|
||||
for (i in 0 until extractor.trackCount) {
|
||||
val trackFormat = extractor.getTrackFormat(i)
|
||||
val mime = trackFormat.getString(MediaFormat.KEY_MIME)
|
||||
if (mime?.startsWith("audio/") == true) {
|
||||
audioTrackIndex = i
|
||||
format = trackFormat
|
||||
break
|
||||
decoder =
|
||||
MediaCodec.createDecoderByType(trackInfo.mime).apply {
|
||||
configure(trackInfo.format, null, null, 0)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
check(audioTrackIndex != -1 && format != null) { "No audio track found in file" }
|
||||
|
||||
extractor.selectTrack(audioTrackIndex)
|
||||
val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm"
|
||||
val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
||||
val durationUs = format.getLong(MediaFormat.KEY_DURATION)
|
||||
val duration = (durationUs / 1_000_000).toInt()
|
||||
|
||||
decoder = MediaCodec.createDecoderByType(mime)
|
||||
decoder.configure(format, null, null, 0)
|
||||
decoder.start()
|
||||
|
||||
val estimatedSamples = (sampleRate.toLong() * durationUs / 1_000_000).toInt()
|
||||
val estimatedSamples = (trackInfo.sampleRate.toLong() * trackInfo.durationUs / 1_000_000).toInt()
|
||||
val pcmSamples = ArrayList<Float>(estimatedSamples)
|
||||
val bufferInfo = MediaCodec.BufferInfo()
|
||||
var inputDone = false
|
||||
var outputDone = false
|
||||
|
||||
while (!outputDone && currentCoroutineContext().isActive) {
|
||||
if (!inputDone) {
|
||||
val inputBufferIndex = decoder.dequeueInputBuffer(10000)
|
||||
if (inputBufferIndex >= 0) {
|
||||
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
|
||||
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
||||
if (sampleSize < 0) {
|
||||
decoder.queueInputBuffer(
|
||||
inputBufferIndex,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
MediaCodec.BUFFER_FLAG_END_OF_STREAM,
|
||||
)
|
||||
inputDone = true
|
||||
} else {
|
||||
val presentationTimeUs = extractor.sampleTime
|
||||
decoder.queueInputBuffer(
|
||||
inputBufferIndex,
|
||||
0,
|
||||
sampleSize,
|
||||
presentationTimeUs,
|
||||
0,
|
||||
)
|
||||
extractor.advance()
|
||||
if (durationUs > 0) {
|
||||
onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val inputFeeder = DecoderInputFeeder(decoder, extractor, trackInfo.durationUs, onProgress)
|
||||
val outputDrainer = DecoderOutputDrainer(decoder, pcmSamples)
|
||||
|
||||
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000)
|
||||
if (outputBufferIndex >= 0) {
|
||||
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
|
||||
val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer()
|
||||
while (shortBuffer.hasRemaining()) {
|
||||
pcmSamples.add(shortBuffer.get() / 32768f)
|
||||
}
|
||||
decoder.releaseOutputBuffer(outputBufferIndex, false)
|
||||
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
|
||||
outputDone = true
|
||||
}
|
||||
}
|
||||
while (!outputDrainer.isDone && currentCoroutineContext().isActive) {
|
||||
inputFeeder.feedInput()
|
||||
outputDrainer.drainOutput()
|
||||
}
|
||||
|
||||
return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration)
|
||||
val duration = (trackInfo.durationUs / 1_000_000).toInt()
|
||||
return DecodedAudio(pcmSamples.toFloatArray(), trackInfo.sampleRate, duration)
|
||||
} finally {
|
||||
try {
|
||||
decoder?.stop()
|
||||
} catch (_: IllegalStateException) {
|
||||
// Decoder was never started
|
||||
}
|
||||
decoder?.release()
|
||||
decoder?.safeStopAndRelease()
|
||||
extractor.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaCodec.safeStopAndRelease() {
|
||||
try {
|
||||
stop()
|
||||
} catch (_: IllegalStateException) {
|
||||
// Decoder was never started
|
||||
}
|
||||
release()
|
||||
}
|
||||
|
||||
private fun processPcmWithTarsos(
|
||||
pcmData: FloatArray,
|
||||
preset: VoicePreset,
|
||||
@@ -344,110 +402,159 @@ class VoiceAnonymizer {
|
||||
return waveform
|
||||
}
|
||||
|
||||
private class EncoderInputFeeder(
|
||||
private val encoder: MediaCodec,
|
||||
private val pcmData: FloatArray,
|
||||
private val sampleRate: Int,
|
||||
private val onProgress: (Float) -> Unit,
|
||||
) {
|
||||
private var inputOffset = 0
|
||||
var isDone = false
|
||||
private set
|
||||
|
||||
fun feedInput() {
|
||||
if (isDone) return
|
||||
|
||||
val inputBufferIndex = encoder.dequeueInputBuffer(10000)
|
||||
if (inputBufferIndex < 0) return
|
||||
|
||||
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
|
||||
inputBuffer.clear()
|
||||
|
||||
val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset)
|
||||
if (samplesToWrite <= 0) {
|
||||
queueEndOfStream(inputBufferIndex)
|
||||
} else {
|
||||
queueSampleData(inputBufferIndex, inputBuffer, samplesToWrite)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueEndOfStream(inputBufferIndex: Int) {
|
||||
encoder.queueInputBuffer(
|
||||
inputBufferIndex,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
MediaCodec.BUFFER_FLAG_END_OF_STREAM,
|
||||
)
|
||||
isDone = true
|
||||
}
|
||||
|
||||
private fun queueSampleData(
|
||||
inputBufferIndex: Int,
|
||||
inputBuffer: java.nio.ByteBuffer,
|
||||
samplesToWrite: Int,
|
||||
) {
|
||||
writePcmSamplesToBuffer(inputBuffer, samplesToWrite)
|
||||
val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate
|
||||
encoder.queueInputBuffer(inputBufferIndex, 0, inputBuffer.position(), presentationTimeUs, 0)
|
||||
inputOffset += samplesToWrite
|
||||
onProgress(inputOffset.toFloat() / pcmData.size)
|
||||
}
|
||||
|
||||
private fun writePcmSamplesToBuffer(
|
||||
inputBuffer: java.nio.ByteBuffer,
|
||||
samplesToWrite: Int,
|
||||
) {
|
||||
for (i in 0 until samplesToWrite) {
|
||||
val sample =
|
||||
(pcmData[inputOffset + i] * 32767)
|
||||
.toInt()
|
||||
.coerceIn(-32768, 32767)
|
||||
.toShort()
|
||||
inputBuffer.putShort(sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EncoderOutputDrainer(
|
||||
private val encoder: MediaCodec,
|
||||
private val muxer: MediaMuxer,
|
||||
) {
|
||||
private val bufferInfo = MediaCodec.BufferInfo()
|
||||
private var audioTrackIndex = -1
|
||||
var isMuxerStarted = false
|
||||
private set
|
||||
var isDone = false
|
||||
private set
|
||||
|
||||
fun drainOutput() {
|
||||
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000)
|
||||
|
||||
when {
|
||||
outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> startMuxer()
|
||||
outputBufferIndex >= 0 -> processOutputBuffer(outputBufferIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMuxer() {
|
||||
audioTrackIndex = muxer.addTrack(encoder.outputFormat)
|
||||
muxer.start()
|
||||
isMuxerStarted = true
|
||||
}
|
||||
|
||||
private fun processOutputBuffer(outputBufferIndex: Int) {
|
||||
val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!!
|
||||
writeToMuxerIfReady(outputBuffer)
|
||||
encoder.releaseOutputBuffer(outputBufferIndex, false)
|
||||
checkForEndOfStream()
|
||||
}
|
||||
|
||||
private fun writeToMuxerIfReady(outputBuffer: java.nio.ByteBuffer) {
|
||||
if (isMuxerStarted && bufferInfo.size > 0) {
|
||||
outputBuffer.position(bufferInfo.offset)
|
||||
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
|
||||
muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForEndOfStream() {
|
||||
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
|
||||
isDone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAacFormat(sampleRate: Int): MediaFormat =
|
||||
MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, CHANNELS).apply {
|
||||
setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
||||
setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
|
||||
}
|
||||
|
||||
private fun encodePcmToAac(
|
||||
pcmData: FloatArray,
|
||||
sampleRate: Int,
|
||||
outputFile: File,
|
||||
onProgress: (Float) -> Unit,
|
||||
) {
|
||||
val format =
|
||||
MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, CHANNELS)
|
||||
format.setInteger(
|
||||
MediaFormat.KEY_AAC_PROFILE,
|
||||
MediaCodecInfo.CodecProfileLevel.AACObjectLC,
|
||||
)
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
|
||||
|
||||
val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
|
||||
val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
||||
var muxerStarted = false
|
||||
|
||||
try {
|
||||
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
encoder.configure(createAacFormat(sampleRate), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
encoder.start()
|
||||
|
||||
var audioTrackIndex = -1
|
||||
val bufferInfo = MediaCodec.BufferInfo()
|
||||
var inputOffset = 0
|
||||
var inputDone = false
|
||||
var outputDone = false
|
||||
val totalSamples = pcmData.size
|
||||
val inputFeeder = EncoderInputFeeder(encoder, pcmData, sampleRate, onProgress)
|
||||
val outputDrainer = EncoderOutputDrainer(encoder, muxer)
|
||||
|
||||
while (!outputDone) {
|
||||
if (!inputDone) {
|
||||
val inputBufferIndex = encoder.dequeueInputBuffer(10000)
|
||||
if (inputBufferIndex >= 0) {
|
||||
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
|
||||
inputBuffer.clear()
|
||||
|
||||
val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset)
|
||||
if (samplesToWrite <= 0) {
|
||||
encoder.queueInputBuffer(
|
||||
inputBufferIndex,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
MediaCodec.BUFFER_FLAG_END_OF_STREAM,
|
||||
)
|
||||
inputDone = true
|
||||
} else {
|
||||
for (i in 0 until samplesToWrite) {
|
||||
val sample =
|
||||
(pcmData[inputOffset + i] * 32767)
|
||||
.toInt()
|
||||
.coerceIn(-32768, 32767)
|
||||
.toShort()
|
||||
inputBuffer.putShort(sample)
|
||||
}
|
||||
val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate
|
||||
encoder.queueInputBuffer(
|
||||
inputBufferIndex,
|
||||
0,
|
||||
inputBuffer.position(),
|
||||
presentationTimeUs,
|
||||
0,
|
||||
)
|
||||
inputOffset += samplesToWrite
|
||||
onProgress(inputOffset.toFloat() / totalSamples)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000)
|
||||
when {
|
||||
outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||
audioTrackIndex = muxer.addTrack(encoder.outputFormat)
|
||||
muxer.start()
|
||||
muxerStarted = true
|
||||
}
|
||||
|
||||
outputBufferIndex >= 0 -> {
|
||||
val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!!
|
||||
if (muxerStarted && bufferInfo.size > 0) {
|
||||
outputBuffer.position(bufferInfo.offset)
|
||||
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
|
||||
muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo)
|
||||
}
|
||||
encoder.releaseOutputBuffer(outputBufferIndex, false)
|
||||
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
|
||||
outputDone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!outputDrainer.isDone) {
|
||||
inputFeeder.feedInput()
|
||||
outputDrainer.drainOutput()
|
||||
}
|
||||
muxerStarted = outputDrainer.isMuxerStarted
|
||||
} finally {
|
||||
try {
|
||||
encoder.stop()
|
||||
} catch (_: IllegalStateException) {
|
||||
// Encoder was never started
|
||||
}
|
||||
encoder.release()
|
||||
if (muxerStarted) {
|
||||
muxer.stop()
|
||||
}
|
||||
muxer.release()
|
||||
encoder.safeStopAndRelease()
|
||||
muxer.safeStopAndRelease(muxerStarted)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaMuxer.safeStopAndRelease(wasStarted: Boolean) {
|
||||
if (wasStarted) {
|
||||
stop()
|
||||
}
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
private class FloatArrayAudioInputStream(
|
||||
|
||||
Reference in New Issue
Block a user