From 3ec62ad58a3454eef23e44226662277a1c653286 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 26 Oct 2023 14:55:23 +0200 Subject: [PATCH] Use Float instead of Double for all the level metering logic. (#1645) This is in preparation of further changes to the way the audio level is computed and to allow recording and sending of the waveform. The main reasoning behind the change is twofold: 1) We don't need the precision of Double in our context (we just need a rough indication of the changes in audio level to successfully draw a level meter or a waveform in our UI). 2) Performance: It is true that on 64 bit CPUs single operations involving Floats or Doubles take the same amount of time (i.e one clock cycle). But there are other aspects here that vouch in favor of Floats: - A float takes half the space in memory compared to a double, so when storing long lists of them this can add up. - On Android O and greater the ART runtime can "vectorize" certain operations on lists and make use of the CPU's SIMD registers which are generally 128 bits. So by using floats 4 of them can fit and be computed at the same time whilst with doubles only 2 will fit halving the throughput. References: - https://source.android.com/docs/core/runtime/improvements - https://www.slideshare.net/linaroorg/automatic-vectorization-in-art-android-runtime-sfo17216 --- .../composer/VoiceMessageComposerStateProvider.kt | 2 +- .../voicemessages/VoiceMessageComposerPresenterTest.kt | 2 +- .../android/libraries/textcomposer/TextComposer.kt | 2 +- .../textcomposer/components/VoiceMessageRecording.kt | 8 ++++---- .../libraries/textcomposer/model/VoiceMessageState.kt | 2 +- .../libraries/voicerecorder/api/VoiceRecorderState.kt | 2 +- .../libraries/voicerecorder/impl/VoiceRecorderImpl.kt | 2 +- .../voicerecorder/impl/audio/AudioLevelCalculator.kt | 2 +- .../impl/audio/DecibelAudioLevelCalculator.kt | 4 ++-- .../voicerecorder/impl/VoiceRecorderImplTest.kt | 10 +++++----- .../voicerecorder/test/FakeAudioLevelCalculator.kt | 6 +++--- .../libraries/voicerecorder/test/FakeVoiceRecorder.kt | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt index 01414a6dda..c448ca1a84 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt @@ -23,7 +23,7 @@ import kotlin.time.Duration.Companion.seconds internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5)), + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5f)), ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt index 58a7f54d48..ce965f32dc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt @@ -63,7 +63,7 @@ class VoiceMessageComposerPresenterTest { companion object { private val RECORDING_DURATION = 1.seconds - private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, 0.2) + private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, 0.2f) } @Test diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 6e774bcda4..abb81fc090 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -778,7 +778,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { enableVoiceMessages = true, ) PreviewColumn(items = persistentListOf({ - VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5)) + VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5f)) }, { VoicePreview(voiceMessageState = VoiceMessageState.Preview) }, { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 99a0e82b7c..22244cd58b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -42,7 +42,7 @@ import kotlin.time.Duration.Companion.seconds @Composable internal fun VoiceMessageRecording( - level: Double, + level: Float, duration: Duration, modifier: Modifier = Modifier, ) { @@ -79,7 +79,7 @@ internal fun VoiceMessageRecording( @Composable private fun DebugAudioLevel( - level: Double, + level: Float, modifier: Modifier = Modifier, ) { Box( @@ -89,7 +89,7 @@ private fun DebugAudioLevel( Box( modifier = Modifier .align(Alignment.CenterEnd) - .fillMaxWidth(level.toFloat()) + .fillMaxWidth(level) .background(ElementTheme.colors.iconQuaternary, shape = MaterialTheme.shapes.small) .fillMaxHeight() ) @@ -108,5 +108,5 @@ private fun RedRecordingDot( @PreviewsDayNight @Composable internal fun VoiceMessageRecordingPreview() = ElementPreview { - VoiceMessageRecording(0.5, 0.seconds) + VoiceMessageRecording(0.5f, 0.seconds) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index 590d5f2e50..fce53e2af4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -25,6 +25,6 @@ sealed class VoiceMessageState { data object Sending: VoiceMessageState() data class Recording( val duration: Duration, - val level: Double, + val level: Float, ): VoiceMessageState() } diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt index 6ba1476ac7..6f40677beb 100644 --- a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt @@ -31,7 +31,7 @@ sealed class VoiceRecorderState { * @property elapsedTime The elapsed time since the recording started. * @property level The current audio level of the recording as a fraction of 1. */ - data class Recording(val elapsedTime: Duration, val level: Double) : VoiceRecorderState() + data class Recording(val elapsedTime: Duration, val level: Float) : VoiceRecorderState() /** * The recorder has finished recording. diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index b150cd1059..f8490c6e2a 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -98,7 +98,7 @@ class VoiceRecorderImpl @Inject constructor( } is Audio.Error -> { Timber.e("Voice message error: code=${audio.audioRecordErrorCode}") - _state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0)) + _state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0f)) } } } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt index 554b6ba4b1..efcb2f5570 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt @@ -24,5 +24,5 @@ interface AudioLevelCalculator { * * @return A value between 0 and 1. */ - fun calculateAudioLevel(buffer: ShortArray): Double + fun calculateAudioLevel(buffer: ShortArray): Float } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt index 8a16acf83b..045e6f0e0b 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt @@ -29,7 +29,7 @@ class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { private const val REFERENCE_DB = 50.0 // Reference dB for normal conversation } - override fun calculateAudioLevel(buffer: ShortArray): Double { + override fun calculateAudioLevel(buffer: ShortArray): Float { val rms = buffer.rootMeanSquare() // Convert to decibels and clip @@ -37,7 +37,7 @@ class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { val clipped = min(db, REFERENCE_DB) // Scale to the range [0.0, 1.0] - return clipped / REFERENCE_DB + return (clipped / REFERENCE_DB).toFloat() } private fun ShortArray.rootMeanSquare(): Double { diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt index 5c9e0506ab..3c45771cd2 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -60,11 +60,11 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0f)) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0f)) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0f)) } } @@ -75,9 +75,9 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0f)) timeSource += 29.minutes - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0f)) timeSource += 1.minutes assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt index 1615067f6c..90a5735a5f 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt @@ -19,8 +19,8 @@ package io.element.android.libraries.voicerecorder.test import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator import kotlin.math.abs -class FakeAudioLevelCalculator: AudioLevelCalculator { - override fun calculateAudioLevel(buffer: ShortArray): Double { - return buffer.map { abs(it.toDouble()) }.average() / Short.MAX_VALUE +class FakeAudioLevelCalculator : AudioLevelCalculator { + override fun calculateAudioLevel(buffer: ShortArray): Float { + return buffer.map { abs(it.toFloat()) }.average().toFloat() / Short.MAX_VALUE } } diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index 04153f3a9a..aa2e1e5e0d 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -29,7 +29,7 @@ import kotlin.time.TestTimeSource class FakeVoiceRecorder( private val timeSource: TestTimeSource = TestTimeSource(), private val recordingDuration: Duration = 0.seconds, - private val levels: List = listOf(0.1, 0.2) + private val levels: List = listOf(0.1f, 0.2f) ) : VoiceRecorder { private val _state = MutableStateFlow(VoiceRecorderState.Idle) override val state: StateFlow = _state