Improve VoiceRecorder abstraction
This commit is contained in:
parent
33a021c8ed
commit
e775404e35
@ -55,8 +55,8 @@ class AudioMessageHelper @Inject constructor(
|
|||||||
private var amplitudeTicker: CountUpTimer? = null
|
private var amplitudeTicker: CountUpTimer? = null
|
||||||
private var playbackTicker: CountUpTimer? = null
|
private var playbackTicker: CountUpTimer? = null
|
||||||
|
|
||||||
fun initializeRecorder(attachmentData: ContentAttachmentData) {
|
fun initializeRecorder(roomId: String, attachmentData: ContentAttachmentData) {
|
||||||
voiceRecorder.initializeRecord(attachmentData)
|
voiceRecorder.initializeRecord(roomId, attachmentData)
|
||||||
amplitudeList.clear()
|
amplitudeList.clear()
|
||||||
attachmentData.waveform?.let {
|
attachmentData.waveform?.let {
|
||||||
amplitudeList.addAll(it)
|
amplitudeList.addAll(it)
|
||||||
|
@ -943,7 +943,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
||||||
audioMessageHelper.initializeRecorder(attachmentData)
|
audioMessageHelper.initializeRecorder(room.roomId, attachmentData)
|
||||||
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,77 +17,24 @@
|
|||||||
package im.vector.app.features.voice
|
package im.vector.app.features.voice
|
||||||
|
|
||||||
import android.content.Context
|
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.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.util.md5
|
import org.matrix.android.sdk.api.util.md5
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class AbstractVoiceRecorder(
|
abstract class AbstractVoiceRecorder(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val filenameExt: String,
|
|
||||||
) : VoiceRecorder {
|
) : VoiceRecorder {
|
||||||
|
|
||||||
private val outputDirectory: File by lazy { ensureAudioDirectory(context) }
|
private val outputDirectory: File by lazy { ensureAudioDirectory(context) }
|
||||||
|
protected var outputFile: File? = null
|
||||||
|
|
||||||
protected var mediaRecorder: MediaRecorder? = null
|
override fun initializeRecord(roomId: String, attachmentData: ContentAttachmentData?) {
|
||||||
private var outputFile: File? = null
|
if (attachmentData != null) {
|
||||||
|
outputFile = attachmentData.findVoiceFile(outputDirectory)
|
||||||
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) {
|
|
||||||
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() {
|
override fun cancelRecord() {
|
||||||
stopRecord()
|
stopRecord()
|
||||||
|
|
||||||
@ -95,11 +42,15 @@ abstract class AbstractVoiceRecorder(
|
|||||||
outputFile = null
|
outputFile = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMaxAmplitude(): Int {
|
|
||||||
return mediaRecorder?.maxAmplitude ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getVoiceMessageFile(): File? {
|
override fun getVoiceMessageFile(): File? {
|
||||||
return outputFile
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,11 +22,19 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface VoiceRecorder {
|
interface VoiceRecorder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize recording with a pre-recorded file.
|
* Audio file extension (eg. `mp4`).
|
||||||
* @param attachmentData data of the recorded file
|
|
||||||
*/
|
*/
|
||||||
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.
|
* Start the recording.
|
||||||
|
@ -31,10 +31,6 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
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
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,16 +40,13 @@ class VoiceRecorderL(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
coroutineContext: CoroutineContext,
|
coroutineContext: CoroutineContext,
|
||||||
private val codec: OggOpusEncoder,
|
private val codec: OggOpusEncoder,
|
||||||
) : VoiceRecorder {
|
) : AbstractVoiceRecorder(context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val SAMPLE_RATE = SampleRate.Rate48kHz
|
private val SAMPLE_RATE = SampleRate.Rate48kHz
|
||||||
private const val BITRATE = 24 * 1024
|
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 val recorderScope = CoroutineScope(coroutineContext)
|
||||||
private var recordingJob: Job? = null
|
private var recordingJob: Job? = null
|
||||||
|
|
||||||
@ -65,6 +58,8 @@ class VoiceRecorderL(
|
|||||||
private var bufferSizeInShorts = 0
|
private var bufferSizeInShorts = 0
|
||||||
private var maxAmplitude = 0
|
private var maxAmplitude = 0
|
||||||
|
|
||||||
|
override val fileNameExt: String = "ogg"
|
||||||
|
|
||||||
private fun initializeCodec(filePath: String) {
|
private fun initializeCodec(filePath: String) {
|
||||||
codec.init(filePath, SAMPLE_RATE)
|
codec.init(filePath, SAMPLE_RATE)
|
||||||
codec.setBitrate(BITRATE)
|
codec.setBitrate(BITRATE)
|
||||||
@ -86,19 +81,10 @@ class VoiceRecorderL(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initializeRecord(attachmentData: ContentAttachmentData) {
|
|
||||||
outputFile = attachmentData.findVoiceFile(outputDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startRecord(roomId: String) {
|
override fun startRecord(roomId: String) {
|
||||||
val fileName = "${UUID.randomUUID()}.ogg"
|
outputFile = createOutputFile(roomId).also {
|
||||||
val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply {
|
initializeCodec(it.absolutePath)
|
||||||
mkdirs()
|
|
||||||
}
|
}
|
||||||
val outputFile = File(outputDirectoryForRoom, fileName)
|
|
||||||
this.outputFile = outputFile
|
|
||||||
|
|
||||||
initializeCodec(outputFile.absolutePath)
|
|
||||||
|
|
||||||
recordingJob = recorderScope.launch {
|
recordingJob = recorderScope.launch {
|
||||||
audioRecorder?.startRecording()
|
audioRecorder?.startRecording()
|
||||||
@ -140,19 +126,10 @@ class VoiceRecorderL(
|
|||||||
codec.release()
|
codec.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelRecord() {
|
|
||||||
outputFile?.delete()
|
|
||||||
outputFile = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMaxAmplitude(): Int {
|
override fun getMaxAmplitude(): Int {
|
||||||
return maxAmplitude
|
return maxAmplitude
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVoiceMessageFile(): File? {
|
|
||||||
return outputFile
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createAudioRecord() {
|
private fun createAudioRecord() {
|
||||||
val channelConfig = AudioFormat.CHANNEL_IN_MONO
|
val channelConfig = AudioFormat.CHANNEL_IN_MONO
|
||||||
val format = AudioFormat.ENCODING_PCM_16BIT
|
val format = AudioFormat.ENCODING_PCM_16BIT
|
||||||
|
@ -20,26 +20,17 @@ import android.content.Context
|
|||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
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)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
class VoiceRecorderQ(context: Context) : AbstractVoiceRecorder(context, "ogg") {
|
class VoiceRecorderQ(context: Context) : AbstractVoiceRecorderQ(context) {
|
||||||
|
|
||||||
override fun pauseRecord() {
|
// We can directly use OGG here
|
||||||
// Can throw when the record is less than 1 second.
|
override val outputFormat = MediaRecorder.OutputFormat.OGG
|
||||||
tryOrNull { mediaRecorder?.pause() }
|
override val audioEncoder = MediaRecorder.AudioEncoder.OPUS
|
||||||
}
|
|
||||||
|
|
||||||
override fun resumeRecord() {
|
override val fileNameExt: String = "ogg"
|
||||||
mediaRecorder?.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setOutputFormat(mediaRecorder: MediaRecorder) {
|
|
||||||
// We can directly use OGG here
|
|
||||||
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG)
|
|
||||||
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user