From 2430f725d63299d87f8ab946e143b1806da431ce Mon Sep 17 00:00:00 2001 From: Julius Linus Date: Fri, 21 Jul 2023 16:23:54 -0500 Subject: [PATCH 1/2] AudioUtils and Waveform Seekbar - Created AudioUtils for processing audio messages - Created Waveform Seekbar, for visualizing a FloatArray - Time limit of about 5 seconds, else shows regular seekbar - Also made mediaPlayer smoother - Fixed time discontinuity bug when playing voice messages, but only on API 28 or higher Signed-off-by: Julius Linus --- .../IncomingVoiceMessageViewHolder.kt | 14 +- .../OutcomingVoiceMessageViewHolder.kt | 14 +- .../com/nextcloud/talk/chat/ChatActivity.kt | 76 ++++++-- .../talk/models/json/chat/ChatMessage.kt | 20 ++- .../com/nextcloud/talk/ui/WaveformSeekBar.kt | 115 ++++++++++++ .../ui/theme/TalkSpecificViewThemeUtils.kt | 13 ++ .../com/nextcloud/talk/utils/AudioUtils.kt | 167 ++++++++++++++++++ .../item_custom_incoming_voice_message.xml | 61 +++++-- .../item_custom_outcoming_voice_message.xml | 65 ++++--- app/src/main/res/values/styles.xml | 7 - 10 files changed, 475 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 0cab26b6d..af58ee4dc 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -101,8 +101,8 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : setParentMessageDataOnMessageItem(message) updateDownloadState(message) - binding.seekbar.max = message.voiceMessageDuration - 1 - viewThemeUtils.platform.themeHorizontalSeekBar(binding.seekbar) + binding.seekbar.max = message.voiceMessageDuration * ONE_SEC + viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) if (message.isPlayingVoiceMessage) { @@ -115,7 +115,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : val t = message.voiceMessagePlayedSeconds.toLong() binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) binding.voiceMessageDuration.visibility = View.VISIBLE - binding.seekbar.setProgress(message.voiceMessagePlayedSeconds, true) + binding.seekbar.progress = message.voiceMessageSeekbarProgress } else { binding.playPauseBtn.visibility = View.VISIBLE binding.playPauseBtn.icon = ContextCompat.getDrawable( @@ -127,6 +127,11 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : if (message.isDownloadingVoiceMessage) { showVoiceMessageLoading() } else { + if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) { + binding.seekbar.setWaveData(FloatArray(0)) + } else { + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } binding.progressBar.visibility = View.GONE } @@ -139,7 +144,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : binding.seekbar.progress = SEEKBAR_START message.resetVoiceMessage = false message.voiceMessagePlayedSeconds = 0 - binding.voiceMessageDuration.visibility = View.GONE + binding.voiceMessageDuration.visibility = View.INVISIBLE } binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { @@ -330,5 +335,6 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : companion object { private const val TAG = "VoiceInMessageView" private const val SEEKBAR_START: Int = 0 + private const val ONE_SEC: Int = 1000 } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt index cf1ee2bba..ea6d1114a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt @@ -98,8 +98,8 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : setParentMessageDataOnMessageItem(message) updateDownloadState(message) - binding.seekbar.max = message.voiceMessageDuration - 1 - viewThemeUtils.platform.themeHorizontalSeekBar(binding.seekbar) + binding.seekbar.max = message.voiceMessageDuration * ONE_SEC + viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) handleIsPlayingVoiceMessageState(message) @@ -176,7 +176,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : ) binding.seekbar.progress = SEEKBAR_START message.voiceMessagePlayedSeconds = 0 - binding.voiceMessageDuration.visibility = View.GONE + binding.voiceMessageDuration.visibility = View.INVISIBLE message.resetVoiceMessage = false } } @@ -185,6 +185,11 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : if (message.isDownloadingVoiceMessage) { showVoiceMessageLoading() } else { + if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) { + binding.seekbar.setWaveData(FloatArray(0)) + } else { + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } binding.progressBar.visibility = View.GONE } } @@ -201,7 +206,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : val t = message.voiceMessagePlayedSeconds.toLong() binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) binding.voiceMessageDuration.visibility = View.VISIBLE - binding.seekbar.setProgress(message.voiceMessagePlayedSeconds, true) + binding.seekbar.progress = message.voiceMessageSeekbarProgress } else { binding.playPauseBtn.visibility = View.VISIBLE binding.playPauseBtn.icon = ContextCompat.getDrawable( @@ -313,5 +318,6 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : companion object { private const val TAG = "VoiceOutMessageView" private const val SEEKBAR_START: Int = 0 + private const val ONE_SEC: Int = 1000 } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 013da8f14..2a2418ec3 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -188,6 +188,7 @@ import com.nextcloud.talk.ui.dialog.ShowReactionsDialog import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.AudioUtils import com.nextcloud.talk.utils.ContactUtils import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.DateConstants @@ -232,6 +233,10 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import retrofit2.HttpException @@ -343,6 +348,11 @@ class ChatActivity : AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT ) + + // messy workaround for a mediaPlayer bug, don't delete + private var lastRecordMediaPosition: Int = 0 + private var lastRecordedSeeked: Boolean = false + private lateinit var participantPermissions: ParticipantPermissions private var videoURI: Uri? = null @@ -861,21 +871,45 @@ class ChatActivity : adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } adapter?.registerViewClickListener( R.id.playPauseBtn - ) { view, message -> + ) { _, message -> val filename = message.selectedIndividualHashMap!!["name"] val file = File(context.cacheDir, filename!!) if (file.exists()) { if (message.isPlayingVoiceMessage) { pausePlayback(message) } else { - startPlayback(message) + setUpWaveform(message) } } else { + Log.d(TAG, "Downloaded to cache") downloadFileToCache(message) } } } + private fun setUpWaveform(message: ChatMessage) { + val filename = message.selectedIndividualHashMap!!["name"] + val file = File(context.cacheDir, filename!!) + if (file.exists() && message.voiceMessageFloatArray == null) { + message.isDownloadingVoiceMessage = true + adapter?.update(message) + CoroutineScope(Dispatchers.Default).launch { + val bars = if (message.actorDisplayName == conversationUser?.displayName) { + NUM_BARS_OUTCOMING + } else { + NUM_BARS_INCOMING + } + val r = AudioUtils.audioFileToFloatArray(file, bars) + message.voiceMessageFloatArray = r + withContext(Dispatchers.Main) { + startPlayback(message) + } + } + } else { + startPlayback(message) + } + } + private fun initMessageHolders(): MessageHolders { val messageHolders = MessageHolders() val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!) @@ -1215,7 +1249,6 @@ class ChatActivity : setDataSource(currentVoiceRecordFile) prepare() setOnPreparedListener { - Log.d(TAG, "Julius the duration is ${it.duration}") binding.messageInputView.seekBar.progress = 0 binding.messageInputView.seekBar.max = it.duration voicePreviewObjectAnimator = ObjectAnimator.ofInt( @@ -1742,6 +1775,7 @@ class ChatActivity : mediaPlayer?.let { if (!it.isPlaying) { it.start() + Log.d(TAG, "MediaPlayer has Started") } mediaPlayerHandler = Handler() @@ -1751,17 +1785,20 @@ class ChatActivity : if (message.isPlayingVoiceMessage) { val pos = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE if (pos < (mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE)) { + lastRecordMediaPosition = mediaPlayer!!.currentPosition message.voiceMessagePlayedSeconds = pos + message.voiceMessageSeekbarProgress = mediaPlayer!!.currentPosition adapter?.update(message) } else { message.resetVoiceMessage = true message.voiceMessagePlayedSeconds = 0 + message.voiceMessageSeekbarProgress = 0 adapter?.update(message) stopMediaPlayer(message) } } } - mediaPlayerHandler.postDelayed(this, SECOND) + mediaPlayerHandler.postDelayed(this, 15) } }) @@ -1774,6 +1811,7 @@ class ChatActivity : private fun pausePlayback(message: ChatMessage) { if (mediaPlayer!!.isPlaying) { mediaPlayer!!.pause() + Log.d(TAG, "MediaPlayer is paused") } message.isPlayingVoiceMessage = false @@ -1794,13 +1832,22 @@ class ChatActivity : mediaPlayer = MediaPlayer().apply { setDataSource(absolutePath) prepare() - } - - currentlyPlayedVoiceMessage = message - message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE - - mediaPlayer!!.setOnCompletionListener { - stopMediaPlayer(message) + setOnPreparedListener { + currentlyPlayedVoiceMessage = message + message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE + lastRecordedSeeked = false + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setOnMediaTimeDiscontinuityListener { mp, _ -> + if (lastRecordMediaPosition > ONE_SECOND_IN_MILLIS && !lastRecordedSeeked) { + mp.seekTo(lastRecordMediaPosition) + lastRecordedSeeked = true + } + } + } + setOnCompletionListener { + stopMediaPlayer(message) + } } } catch (e: Exception) { Log.e(TAG, "failed to initialize mediaPlayer", e) @@ -1836,7 +1883,7 @@ class ChatActivity : override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) { if (mediaPlayer != null) { if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) { - mediaPlayer!!.seekTo(progress * VOICE_MESSAGE_SEEKBAR_BASE) + mediaPlayer!!.seekTo(progress) } } } @@ -1894,7 +1941,8 @@ class ChatActivity : WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id) .observeForever { workInfo: WorkInfo -> if (workInfo.state == WorkInfo.State.SUCCEEDED) { - startPlayback(message) + setUpWaveform(message) + // startPlayback(message) } } } @@ -4227,5 +4275,7 @@ class ChatActivity : private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping" private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping" + private const val NUM_BARS_OUTCOMING: Int = 38 + private const val NUM_BARS_INCOMING: Int = 50 } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt index c61a0c278..e38cffda1 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt @@ -42,7 +42,6 @@ import com.stfalcon.chatkit.commons.models.IUser import com.stfalcon.chatkit.commons.models.MessageContentType import kotlinx.parcelize.Parcelize import java.security.MessageDigest -import java.util.Arrays import java.util.Date @Parcelize @@ -132,7 +131,11 @@ data class ChatMessage( var voiceMessagePlayedSeconds: Int = 0, - var voiceMessageDownloadProgress: Int = 0 + var voiceMessageDownloadProgress: Int = 0, + + var voiceMessageSeekbarProgress: Int = 0, + + var voiceMessageFloatArray: FloatArray? = null ) : Parcelable, MessageContentType, MessageContentType.Image { @@ -140,7 +143,7 @@ data class ChatMessage( // messageTypesToIgnore is weird. must be deleted by refactoring!!! @JsonIgnore - var messageTypesToIgnore = Arrays.asList( + var messageTypesToIgnore = listOf( MessageType.REGULAR_TEXT_MESSAGE, MessageType.SYSTEM_MESSAGE, MessageType.SINGLE_LINK_VIDEO_MESSAGE, @@ -417,6 +420,17 @@ data class ChatMessage( return map != null && MessageDigest.isEqual(map[key]!!.toByteArray(), searchTerm.toByteArray()) } + // needed a equals and hashcode function to fix detekt errors + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return false + } + + override fun hashCode(): Int { + return 0 + } + val isVoiceMessage: Boolean get() = "voice-message" == messageType val isCommandMessage: Boolean diff --git a/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt b/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt new file mode 100644 index 000000000..6cf10456a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.ui + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatSeekBar +import kotlin.math.roundToInt + +class WaveformSeekBar : AppCompatSeekBar { + + @ColorInt + private var primary: Int = Color.parseColor("#679ff5") + + @ColorInt + private var secondary: Int = Color.parseColor("#a6c6f7") + private var waveData: FloatArray = floatArrayOf() + private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init() + } + + fun setColors(@ColorInt p: Int, @ColorInt s: Int) { + primary = p + secondary = s + invalidate() + } + + fun setWaveData(data: FloatArray) { + waveData = data + invalidate() + } + + private fun init() { + paint.apply { + strokeCap = Paint.Cap.ROUND + strokeWidth = DEFAULT_BAR_WIDTH.dp.toFloat() + color = Color.RED + } + } + + override fun onDraw(canvas: Canvas?) { + if (waveData.isEmpty() || waveData[0].toString() == "NaN") { + super.onDraw(canvas) + } else { + if (progressDrawable != null) { + super.setProgressDrawable(null) + } + + drawWaveformSeekbar(canvas) + super.onDraw(canvas) + } + } + + private fun drawWaveformSeekbar(canvas: Canvas?) { + val usableHeight = height - paddingTop - paddingBottom + val usableWidth = width - paddingLeft - paddingRight + val midpoint = usableHeight / 2f + val maxHeight: Float = usableHeight / MAX_HEIGHT_DIVISOR + val barGap: Float = (usableWidth - waveData.size * DEFAULT_BAR_WIDTH) / (waveData.size - 1).toFloat() + + canvas?.apply { + save() + translate(paddingLeft.toFloat(), paddingTop.toFloat()) + for (i in waveData.indices) { + val x: Float = i * (DEFAULT_BAR_WIDTH + barGap) + DEFAULT_BAR_WIDTH / 2f + val y: Float = waveData[i] * maxHeight + val progress = (x / usableWidth) + paint.color = if (progress * max < getProgress()) primary else secondary + canvas.drawLine(x, midpoint - y, x, midpoint + y, paint) + } + + restore() + } + } + + companion object { + private const val DEFAULT_BAR_WIDTH: Int = 2 + private const val MAX_HEIGHT_DIVISOR: Float = 4.0f + private val Int.dp: Int + get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt index 179027f40..99fb803c7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt @@ -24,6 +24,8 @@ package com.nextcloud.talk.ui.theme import android.annotation.TargetApi import android.content.Context import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.os.Build @@ -47,6 +49,7 @@ import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils import com.nextcloud.talk.R import com.nextcloud.talk.ui.MicInputCloud +import com.nextcloud.talk.ui.WaveformSeekBar import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils import com.vanniktech.emoji.EmojiTextView @@ -250,6 +253,16 @@ class TalkSpecificViewThemeUtils @Inject constructor( } } + fun themeWaveFormSeekBar(waveformSeekBar: WaveformSeekBar) { + withScheme(waveformSeekBar) { scheme -> + waveformSeekBar.thumb.colorFilter = + PorterDuffColorFilter(scheme.inversePrimary, PorterDuff.Mode.SRC_IN) + waveformSeekBar.setColors(scheme.inversePrimary, scheme.onPrimaryContainer) + waveformSeekBar.progressDrawable?.colorFilter = + PorterDuffColorFilter(scheme.primary, PorterDuff.Mode.SRC_IN) + } + } + companion object { private val THEMEABLE_PLACEHOLDER_IDS = listOf( R.drawable.ic_mimetype_package_x_generic, diff --git a/app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt new file mode 100644 index 000000000..8399cd0c3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt @@ -0,0 +1,167 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.utils + +import android.media.AudioFormat +import android.media.MediaCodec +import android.media.MediaCodec.CodecException +import android.media.MediaCodecList +import android.media.MediaExtractor +import android.media.MediaFormat +import android.os.SystemClock +import android.util.Log +import java.io.File +import java.io.IOException +import java.nio.ByteOrder +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlin.math.abs + +/** + * AudioUtils are for processing raw audio using android's low level APIs, for more information read here + * [MediaCodec documentation](https://developer.android.com/reference/android/media/MediaCodec) + */ +object AudioUtils { + private val TAG = AudioUtils::class.java.simpleName + private const val VALUE_10 = 10 + private const val TIME_LIMIT = 5000 + + /** + * Suspension function, returns a FloatArray containing the values of an audio file squeezed between [0,1) + */ + @Throws(IOException::class) + suspend fun audioFileToFloatArray(file: File, size: Int): FloatArray { + return suspendCoroutine { + val startTime = SystemClock.elapsedRealtime() + var result = mutableListOf() + val path = file.path + val mediaExtractor = MediaExtractor() + mediaExtractor.setDataSource(path) + + val mediaFormat = mediaExtractor.getTrackFormat(0) + mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null) + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 0) + + mediaExtractor.release() + + val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS) + val codecName = mediaCodecList.findDecoderForFormat(mediaFormat) + val mediaCodec = MediaCodec.createByCodecName(codecName) + mediaCodec.setCallback(object : MediaCodec.Callback() { + private var extractor: MediaExtractor? = null + val tempList = mutableListOf() + override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { + if (extractor == null) { + extractor = MediaExtractor() + try { + extractor!!.setDataSource(path) + extractor!!.selectTrack(0) + } catch (e: IOException) { + e.printStackTrace() + } + } + val byteBuffer = codec.getInputBuffer(index) + if (byteBuffer != null) { + val sampleSize = extractor!!.readSampleData(byteBuffer, 0) + if (sampleSize > 0) { + val isOver = !extractor!!.advance() + codec.queueInputBuffer( + index, + 0, + sampleSize, + extractor!!.sampleTime, + if (isOver) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0 + ) + } + } + } + + override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { + val outputBuffer = codec.getOutputBuffer(index) + val bufferFormat = codec.getOutputFormat(index) + val samples = outputBuffer!!.order(ByteOrder.nativeOrder()).asShortBuffer() + val numChannels = bufferFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) + if (index < 0 || index >= numChannels) { + return + } + val sampleLength = (samples.remaining() / numChannels) + // Squeezes the value of each sample between [0,1) using y = (x-1)/x + for (i in 0 until sampleLength) { + val x = abs(samples[i * numChannels + index].toInt()) / VALUE_10 + val y = (if (x > 0) ((x - 1) / x.toFloat()) else x.toFloat()) + tempList.add(y) + } + codec.releaseOutputBuffer(index, false) + val currTime = SystemClock.elapsedRealtime() - startTime + if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM > 0 || currTime > TIME_LIMIT) { + codec.stop() + codec.release() + extractor!!.release() + extractor = null + if (currTime < TIME_LIMIT) { + result = tempList + } else { + Log.d(TAG, "time limit exceeded") + } + } + } + + override fun onError(codec: MediaCodec, e: CodecException) { + Log.e(TAG, "Error in MediaCodec Callback: \n$e") + codec.stop() + codec.release() + extractor!!.release() + extractor = null + result = tempList + } + + override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { + // unused atm + } + }) + mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT) + mediaCodec.configure(mediaFormat, null, null, 0) + mediaCodec.start() + while (result.size <= 0) { + continue + } + it.resume(shrinkFloatArray(result.toFloatArray(), size)) + } + } + + private fun shrinkFloatArray(data: FloatArray, size: Int): FloatArray { + val result = FloatArray(size) + val scale = data.size / size + var begin = 0 + var end = scale + for (i in 0 until size) { + val arr = data.copyOfRange(begin, end) + var sum = 0f + for (j in arr.indices) { + sum += arr[j] + } + result[i] = (sum / arr.size) + begin += scale + end += scale + } + + return result + } +} diff --git a/app/src/main/res/layout/item_custom_incoming_voice_message.xml b/app/src/main/res/layout/item_custom_incoming_voice_message.xml index c2e79ab28..ae32f3439 100644 --- a/app/src/main/res/layout/item_custom_incoming_voice_message.xml +++ b/app/src/main/res/layout/item_custom_incoming_voice_message.xml @@ -91,32 +91,55 @@ app:iconSize="40dp" app:iconTint="@color/nc_incoming_text_default" /> - + android:layout_height="70dp" + android:thumb="@drawable/voice_message_outgoing_seek_bar_slider" + tools:progress="50" + android:layout_weight="1" /> - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + + - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + - @color/colorPrimary - - + +