Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 32a6b1cf authored by Luca Stefani's avatar Luca Stefani
Browse files

Recorder: Redo high quality recorder

Change-Id: I05b255e2eb8a327f4056f89e722086ecf6a18f0f
parent 2a69a8b2
Loading
Loading
Loading
Loading
+26 −60
Original line number Diff line number Diff line
@@ -12,37 +12,33 @@ import android.media.MediaRecorder
import android.util.Log
import androidx.annotation.RequiresPermission
import org.lineageos.recorder.utils.PcmConverter
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.file.Files
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.file.Path
import kotlin.math.abs

class HighQualityRecorder : SoundRecording {
    private var record: AudioRecord? = null
    private var pcmConverter: PcmConverter? = null
    private var path: Path? = null
    private var file: File? = null
    private var maxAmplitude = 0
    private var isRecording = false
    private var trackAmplitude = false

    @RequiresPermission(permission.RECORD_AUDIO)
    override fun startRecording(path: Path) {
        this.path = path
        this.file = path.toFile()

        val audioFormat = AudioFormat.Builder()
            .setSampleRate(SAMPLING_RATE)
            .setChannelMask(CHANNEL_IN)
            .setEncoding(FORMAT)
            .build()
        pcmConverter = PcmConverter(
            audioFormat.sampleRate.toLong(),
            audioFormat.channelCount,
            audioFormat.frameSizeInBytes * 8 / audioFormat.channelCount
        )
        record = AudioRecord(
            MediaRecorder.AudioSource.DEFAULT, audioFormat.sampleRate,
            audioFormat.channelMask, audioFormat.encoding, BUFFER_SIZE_IN_BYTES
            audioFormat.channelMask, audioFormat.encoding, BUFFER_SIZE
        ).apply {
            startRecording()
        }
@@ -63,13 +59,6 @@ class HighQualityRecorder : SoundRecording {
        record?.release()
        record = null

        path?.also {
            pcmConverter?.convertToWave(it)
        } ?: run {
            Log.w(TAG, "Null path")
            return false
        }

        return true
    }

@@ -96,56 +85,33 @@ class HighQualityRecorder : SoundRecording {
            if (!trackAmplitude) {
                trackAmplitude = true
            }
            val value = maxAmplitude
            maxAmplitude = 0
            return value
            return maxAmplitude
        }

    private fun recordingThreadImpl() {
        try {
            BufferedOutputStream(Files.newOutputStream(path)).use { out ->
                val data = ByteArray(BUFFER_SIZE_IN_BYTES)
                while (isRecording) {
                    try {
                        val record = record ?: throw NullPointerException("Null record")

                        when (val status = record.read(data, 0, BUFFER_SIZE_IN_BYTES)) {
                            AudioRecord.ERROR_INVALID_OPERATION,
                            AudioRecord.ERROR_BAD_VALUE -> {
                                Log.e(TAG, "Error reading audio record data")
                                isRecording = false
                            }
            FileOutputStream(file).use { out ->
                PcmConverter.writeWavHeader(out, SAMPLING_RATE, CHANNEL_IN)

                            AudioRecord.ERROR_DEAD_OBJECT,
                            AudioRecord.ERROR -> continue
                val buffer = ByteArray(BUFFER_SIZE)
                while (isRecording) {
                    val read = record?.read(buffer, 0, BUFFER_SIZE) ?: 0
                    if (read > 0) {
                        out.write(buffer, 0, read)

                            // Status indicates the number of bytes
                            else -> if (status != 0) {
                        if (trackAmplitude) {
                                    var i = 0
                                    while (i < status) {
                                        val value = abs(
                                            data[i].toInt() or (data[i + 1].toInt() shl 8)
                                        )
                                        if (maxAmplitude < value) {
                                            maxAmplitude = value
                                        }
                                        i += 2
                                    }
                                }
                                out.write(data, 0, status)
                            maxAmplitude = 0
                            for (i in 0 until read step 2) {
                                val sample = ByteBuffer.wrap(buffer, i, 2)
                                    .order(ByteOrder.LITTLE_ENDIAN)
                                    .short
                                    .toInt()
                                maxAmplitude = maxOf(maxAmplitude, abs(sample))
                            }
                        }
                    } catch (e: IOException) {
                        Log.e(TAG, "Failed to write audio stream", e)
                        // Stop recording
                        isRecording = false
                    } catch (e: NullPointerException) {
                        Log.e(TAG, "Null record", e)
                        // Stop recording
                        isRecording = false
                    }
                }
                PcmConverter.updateWavHeader(out)
            }
        } catch (e: IOException) {
            Log.e(TAG, "Can't find output file", e)
@@ -162,7 +128,7 @@ class HighQualityRecorder : SoundRecording {
        private const val SAMPLING_RATE = 44100
        private const val CHANNEL_IN = AudioFormat.CHANNEL_IN_STEREO
        private const val FORMAT = AudioFormat.ENCODING_PCM_16BIT
        private val BUFFER_SIZE_IN_BYTES = 2 * AudioRecord.getMinBufferSize(
        private val BUFFER_SIZE = AudioRecord.getMinBufferSize(
            SAMPLING_RATE,
            CHANNEL_IN,
            FORMAT
+48 −98
Original line number Diff line number Diff line
@@ -5,106 +5,56 @@

package org.lineageos.recorder.utils

import android.util.Log
import java.io.IOException
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path

class PcmConverter(sampleRate: Long, channels: Int, bitsPerSample: Int) {
    private val byteRate = channels * sampleRate * bitsPerSample / 8
    private val wavHeader = byteArrayOf(
        'R'.code.toByte(),
        'I'.code.toByte(),
        'F'.code.toByte(),
        'F'.code.toByte(),
        0,
        0,
        0,
        0,  // data length placeholder
        'W'.code.toByte(),
        'A'.code.toByte(),
        'V'.code.toByte(),
        'E'.code.toByte(),
        'f'.code.toByte(),
        'm'.code.toByte(),
        't'.code.toByte(),
        ' '.code.toByte(),  // 'fmt ' chunk
        16,  // 4 bytes: size of 'fmt ' chunk
        0,
        0,
        0,
        1,  // format = 1
        0,
        channels.toByte(),
        0,
        (sampleRate and 0xffL).toByte(),
        (sampleRate shr 8 and 0xffL).toByte(),
        0,
        0,
        (byteRate and 0xffL).toByte(),
        (byteRate shr 8 and 0xffL).toByte(),
        (byteRate shr 16 and 0xffL).toByte(),
        0,
        (channels * bitsPerSample / 8).toByte(),  // block align
        0,
        bitsPerSample.toByte(),  // bits per sample
        0,
        'd'.code.toByte(),
        'a'.code.toByte(),
        't'.code.toByte(),
        'a'.code.toByte(),
        0,
        0,
        0,
        0
    )

    fun convertToWave(path: Path) {
        val tmpPath = path.parent.resolve("${path.fileName}.tmp")

        try {
            Files.newInputStream(path)?.use { input ->
                Files.newOutputStream(tmpPath)?.use { output ->
                    val audioLength = Files.size(path)
                    val dataLength = audioLength + 36
                    writeWaveHeader(output, audioLength, dataLength)
                    input.copyTo(output)
                }
            }
        } catch (e: IOException) {
            Log.e(TAG, "Failed to convert to wav", e)
        }

        // Now rename tmp file to output destination
        try {
            // Delete old file
            Files.delete(path)
            Files.move(tmpPath, path)
        } catch (e: IOException) {
            Log.e(TAG, "Failed to copy file to output destination", e)
        }
    }

    // http://stackoverflow.com/questions/4440015/java-pcm-to-wav
    @Throws(IOException::class)
    private fun writeWaveHeader(
        out: OutputStream, audioLength: Long,
        dataLength: Long
import android.media.AudioFormat
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder

object PcmConverter {
    fun writeWavHeader(
        out: FileOutputStream,
        sampleRate: Int,
        channelConfig: Int,
    ) {
        val header = wavHeader.copyOf(wavHeader.size)
        header[4] = (dataLength and 0xffL).toByte()
        header[5] = (dataLength shr 8 and 0xffL).toByte()
        header[6] = (dataLength shr 16 and 0xffL).toByte()
        header[7] = (dataLength shr 24 and 0xffL).toByte()
        header[40] = (audioLength and 0xffL).toByte()
        header[41] = (audioLength shr 8 and 0xffL).toByte()
        header[42] = (audioLength shr 16 and 0xffL).toByte()
        header[43] = (audioLength shr 24 and 0xffL).toByte()
        out.write(header, 0, header.size)
    }

    companion object {
        private const val TAG = "PcmConverter"
        val channels = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2

        val header = ByteBuffer.allocate(44)
        header.order(ByteOrder.LITTLE_ENDIAN)

        header.put("RIFF".toByteArray())
        header.putInt(0)  // Placeholder for file size
        header.put("WAVE".toByteArray())
        header.put("fmt ".toByteArray())
        header.putInt(16)  // Sub-chunk size (16 for PCM)
        header.putShort(1) // Audio format (1 for PCM)
        header.putShort(channels.toShort())
        header.putInt(sampleRate)
        header.putInt(sampleRate * channels * 2) // Byte rate
        header.putShort((channels * 2).toShort())
        header.putShort(16)  // Bits per sample
        header.put("data".toByteArray())
        header.putInt(0)  // Placeholder for data size

        out.write(header.array())
    }

    fun updateWavHeader(out: FileOutputStream) {
        val fileSize = out.channel.size()
        val audioLen = fileSize - 44
        val dataLen = audioLen + 36

        // Update file size at offset 4
        val dataLenBuf = ByteBuffer.allocate(4)
        dataLenBuf.order(ByteOrder.LITTLE_ENDIAN)
        dataLenBuf.putInt(0, dataLen.toInt())
        out.channel.position(4)
        out.channel.write(dataLenBuf)

        // Update data size at offset 40
        val audioLenBuf = ByteBuffer.allocate(4)
        audioLenBuf.order(ByteOrder.LITTLE_ENDIAN)
        audioLenBuf.putInt(0, audioLen.toInt())
        out.channel.position(40)
        out.channel.write(audioLenBuf)
    }
}