Loading app/src/main/java/org/lineageos/recorder/service/HighQualityRecorder.kt +26 −60 Original line number Diff line number Diff line Loading @@ -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() } Loading @@ -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 } Loading @@ -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) Loading @@ -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 Loading app/src/main/java/org/lineageos/recorder/utils/PcmConverter.kt +48 −98 Original line number Diff line number Diff line Loading @@ -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) } } Loading
app/src/main/java/org/lineageos/recorder/service/HighQualityRecorder.kt +26 −60 Original line number Diff line number Diff line Loading @@ -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() } Loading @@ -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 } Loading @@ -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) Loading @@ -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 Loading
app/src/main/java/org/lineageos/recorder/utils/PcmConverter.kt +48 −98 Original line number Diff line number Diff line Loading @@ -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) } }