Improve VoiceRecorder abstraction

This commit is contained in:
Florian Renaud 2022-10-13 23:15:06 +02:00
parent 33a021c8ed
commit e775404e35
7 changed files with 163 additions and 112 deletions

View File

@ -55,8 +55,8 @@ class AudioMessageHelper @Inject constructor(
private var amplitudeTicker: CountUpTimer? = null
private var playbackTicker: CountUpTimer? = null
fun initializeRecorder(attachmentData: ContentAttachmentData) {
voiceRecorder.initializeRecord(attachmentData)
fun initializeRecorder(roomId: String, attachmentData: ContentAttachmentData) {
voiceRecorder.initializeRecord(roomId, attachmentData)
amplitudeList.clear()
attachmentData.waveform?.let {
amplitudeList.addAll(it)

View File

@ -943,7 +943,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
audioMessageHelper.initializeRecorder(attachmentData)
audioMessageHelper.initializeRecorder(room.roomId, attachmentData)
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
}

View File

@ -17,75 +17,22 @@
package im.vector.app.features.voice
import android.content.Context
import android.media.MediaRecorder
import android.os.Build
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.util.md5
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
abstract class AbstractVoiceRecorder(
private val context: Context,
private val filenameExt: String,
) : VoiceRecorder {
private val outputDirectory: File by lazy { ensureAudioDirectory(context) }
protected var outputFile: File? = null
protected var mediaRecorder: MediaRecorder? = null
private var outputFile: File? = null
abstract fun setOutputFormat(mediaRecorder: MediaRecorder)
private fun init() {
createMediaRecorder().let {
it.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
setOutputFormat(it)
it.setAudioEncodingBitRate(24000)
it.setAudioSamplingRate(48000)
mediaRecorder = it
}
}
private fun createMediaRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
@Suppress("DEPRECATION")
MediaRecorder()
}
}
override fun initializeRecord(attachmentData: ContentAttachmentData) {
override fun initializeRecord(roomId: String, attachmentData: ContentAttachmentData?) {
if (attachmentData != null) {
outputFile = attachmentData.findVoiceFile(outputDirectory)
}
override fun startRecord(roomId: String) {
init()
val fileName = "${UUID.randomUUID()}.$filenameExt"
val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply {
mkdirs()
}
outputFile = File(outputDirectoryForRoom, fileName)
val mr = mediaRecorder ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mr.setOutputFile(outputFile)
} else {
mr.setOutputFile(FileOutputStream(outputFile).fd)
}
mr.prepare()
mr.start()
}
override fun stopRecord() {
mediaRecorder?.let {
// Can throw when the record is less than 1 second.
tryOrNull { it.stop() }
it.reset()
it.release()
}
mediaRecorder = null
}
override fun cancelRecord() {
@ -95,11 +42,15 @@ abstract class AbstractVoiceRecorder(
outputFile = null
}
override fun getMaxAmplitude(): Int {
return mediaRecorder?.maxAmplitude ?: 0
}
override fun getVoiceMessageFile(): File? {
return outputFile
}
protected fun createOutputFile(roomId: String): File {
val fileName = "${UUID.randomUUID()}.$fileNameExt"
val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply {
mkdirs()
}
return File(outputDirectoryForRoom, fileName)
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.voice
import android.content.Context
import android.media.MediaRecorder
import android.os.Build
import androidx.annotation.RequiresApi
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import java.io.File
/**
* VoiceRecorder abstraction to be used on Android versions >= [Build.VERSION_CODES.Q].
*/
@RequiresApi(Build.VERSION_CODES.Q)
abstract class AbstractVoiceRecorderQ(private val context: Context) : AbstractVoiceRecorder(context) {
var mediaRecorder: MediaRecorder? = null
protected var nextOutputFile: File? = null
private val audioSource: Int = MediaRecorder.AudioSource.DEFAULT
private val audioEncodingBitRate: Int = 24_000
private val audioSamplingRate: Int = 48_000
abstract val outputFormat: Int // see MediaRecorder.OutputFormat
abstract val audioEncoder: Int // see MediaRecorder.AudioEncoder
override fun initializeRecord(roomId: String, attachmentData: ContentAttachmentData?) {
super.initializeRecord(roomId, attachmentData)
mediaRecorder = createMediaRecorder().apply {
setAudioSource(audioSource)
setOutputFormat()
setAudioEncodingBitRate(audioEncodingBitRate)
setAudioSamplingRate(audioSamplingRate)
}
setOutputFile(roomId)
}
override fun startRecord(roomId: String) {
initializeRecord(roomId = roomId)
mediaRecorder?.prepare()
mediaRecorder?.start()
}
override fun pauseRecord() {
// Can throw when the record is less than 1 second.
tryOrNull { mediaRecorder?.pause() }
}
override fun resumeRecord() {
mediaRecorder?.resume()
}
override fun stopRecord() {
// Can throw when the record is less than 1 second.
tryOrNull { mediaRecorder?.stop() }
mediaRecorder?.reset()
release()
}
override fun cancelRecord() {
super.cancelRecord()
nextOutputFile?.delete()
nextOutputFile = null
}
override fun getMaxAmplitude(): Int {
return mediaRecorder?.maxAmplitude ?: 0
}
protected open fun release() {
mediaRecorder?.release()
mediaRecorder = null
}
fun setNextOutputFile(roomId: String) {
val mediaRecorder = mediaRecorder ?: return
nextOutputFile = createOutputFile(roomId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaRecorder.setNextOutputFile(nextOutputFile)
} else {
mediaRecorder.setNextOutputFile(nextOutputFile?.outputStream()?.fd)
}
}
private fun createMediaRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
@Suppress("DEPRECATION")
MediaRecorder()
}
}
private fun MediaRecorder.setOutputFormat() {
setOutputFormat(outputFormat)
setAudioEncoder(audioEncoder)
}
private fun setOutputFile(roomId: String) {
val mediaRecorder = mediaRecorder ?: return
outputFile = outputFile ?: createOutputFile(roomId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mediaRecorder.setOutputFile(outputFile)
} else {
mediaRecorder.setOutputFile(outputFile?.outputStream()?.fd)
}
}
}

View File

@ -22,11 +22,19 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import java.io.File
interface VoiceRecorder {
/**
* Initialize recording with a pre-recorded file.
* @param attachmentData data of the recorded file
* Audio file extension (eg. `mp4`).
*/
fun initializeRecord(attachmentData: ContentAttachmentData)
val fileNameExt: String
/**
* Initialize recording with an optional pre-recorded file.
*
* @param roomId id of the room to initialize record
* @param attachmentData data of the pre-recorded file, if any.
*/
fun initializeRecord(roomId: String, attachmentData: ContentAttachmentData? = null)
/**
* Start the recording.

View File

@ -31,10 +31,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.util.md5
import java.io.File
import java.util.UUID
import kotlin.coroutines.CoroutineContext
/**
@ -44,16 +40,13 @@ class VoiceRecorderL(
private val context: Context,
coroutineContext: CoroutineContext,
private val codec: OggOpusEncoder,
) : VoiceRecorder {
) : AbstractVoiceRecorder(context) {
companion object {
private val SAMPLE_RATE = SampleRate.Rate48kHz
private const val BITRATE = 24 * 1024
}
private val outputDirectory: File by lazy { ensureAudioDirectory(context) }
private var outputFile: File? = null
private val recorderScope = CoroutineScope(coroutineContext)
private var recordingJob: Job? = null
@ -65,6 +58,8 @@ class VoiceRecorderL(
private var bufferSizeInShorts = 0
private var maxAmplitude = 0
override val fileNameExt: String = "ogg"
private fun initializeCodec(filePath: String) {
codec.init(filePath, SAMPLE_RATE)
codec.setBitrate(BITRATE)
@ -86,19 +81,10 @@ class VoiceRecorderL(
}
}
override fun initializeRecord(attachmentData: ContentAttachmentData) {
outputFile = attachmentData.findVoiceFile(outputDirectory)
}
override fun startRecord(roomId: String) {
val fileName = "${UUID.randomUUID()}.ogg"
val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply {
mkdirs()
outputFile = createOutputFile(roomId).also {
initializeCodec(it.absolutePath)
}
val outputFile = File(outputDirectoryForRoom, fileName)
this.outputFile = outputFile
initializeCodec(outputFile.absolutePath)
recordingJob = recorderScope.launch {
audioRecorder?.startRecording()
@ -140,19 +126,10 @@ class VoiceRecorderL(
codec.release()
}
override fun cancelRecord() {
outputFile?.delete()
outputFile = null
}
override fun getMaxAmplitude(): Int {
return maxAmplitude
}
override fun getVoiceMessageFile(): File? {
return outputFile
}
private fun createAudioRecord() {
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val format = AudioFormat.ENCODING_PCM_16BIT

View File

@ -20,26 +20,17 @@ import android.content.Context
import android.media.MediaRecorder
import android.os.Build
import androidx.annotation.RequiresApi
import org.matrix.android.sdk.api.extensions.tryOrNull
/**
* VoiceRecorder to be used on Android versions >= [Build.VERSION_CODES.Q]. It uses the native OPUS support on Android 10+.
* VoiceRecorder to be used on Android versions >= [Build.VERSION_CODES.Q].
* It uses the native OPUS support on Android 10+.
*/
@RequiresApi(Build.VERSION_CODES.Q)
class VoiceRecorderQ(context: Context) : AbstractVoiceRecorder(context, "ogg") {
class VoiceRecorderQ(context: Context) : AbstractVoiceRecorderQ(context) {
override fun pauseRecord() {
// Can throw when the record is less than 1 second.
tryOrNull { mediaRecorder?.pause() }
}
override fun resumeRecord() {
mediaRecorder?.resume()
}
override fun setOutputFormat(mediaRecorder: MediaRecorder) {
// We can directly use OGG here
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG)
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
}
override val outputFormat = MediaRecorder.OutputFormat.OGG
override val audioEncoder = MediaRecorder.AudioEncoder.OPUS
override val fileNameExt: String = "ogg"
}