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
This commit is contained in:
Marco Romano 2023-10-26 14:55:23 +02:00 committed by GitHub
parent b9b3bce2a2
commit 3ec62ad58a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 22 additions and 22 deletions

View file

@ -23,7 +23,7 @@ import kotlin.time.Duration.Companion.seconds
internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> {
override val values: Sequence<VoiceMessageComposerState>
get() = sequenceOf(
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5)),
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5f)),
)
}

View file

@ -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

View file

@ -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)
}, {

View file

@ -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)
}

View file

@ -25,6 +25,6 @@ sealed class VoiceMessageState {
data object Sending: VoiceMessageState()
data class Recording(
val duration: Duration,
val level: Double,
val level: Float,
): VoiceMessageState()
}

View file

@ -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.

View file

@ -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))
}
}
}

View file

@ -24,5 +24,5 @@ interface AudioLevelCalculator {
*
* @return A value between 0 and 1.
*/
fun calculateAudioLevel(buffer: ShortArray): Double
fun calculateAudioLevel(buffer: ShortArray): Float
}

View file

@ -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 {

View file

@ -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"))

View file

@ -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
}
}

View file

@ -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<Double> = listOf(0.1, 0.2)
private val levels: List<Float> = listOf(0.1f, 0.2f)
) : VoiceRecorder {
private val _state = MutableStateFlow<VoiceRecorderState>(VoiceRecorderState.Idle)
override val state: StateFlow<VoiceRecorderState> = _state