Enabling the display for amplitudes in an array of floats for YakBak

This commit is contained in:
Vitor Pamplona
2025-08-20 16:00:06 -04:00
parent 4065adac83
commit da015e3298
13 changed files with 43 additions and 32 deletions

View File

@@ -964,7 +964,7 @@ class Account(
mimeType: String?, mimeType: String?,
hash: String, hash: String,
duration: Int, duration: Int,
waveform: List<Int>, waveform: List<Float>,
) { ) {
signAndComputeBroadcast(VoiceEvent.build(url, mimeType, hash, duration, waveform)) signAndComputeBroadcast(VoiceEvent.build(url, mimeType, hash, duration, waveform))
} }
@@ -974,7 +974,7 @@ class Account(
mimeType: String?, mimeType: String?,
hash: String, hash: String,
duration: Int, duration: Int,
waveform: List<Int>, waveform: List<Float>,
replyTo: EventHintBundle<VoiceEvent>, replyTo: EventHintBundle<VoiceEvent>,
) { ) {
signAndComputeBroadcast(VoiceReplyEvent.build(url, mimeType, hash, duration, waveform, replyTo)) signAndComputeBroadcast(VoiceReplyEvent.build(url, mimeType, hash, duration, waveform, replyTo))

View File

@@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
@Immutable @Immutable
class WaveformData( class WaveformData(
val wave: List<Int>, val wave: List<Float>,
) )
@Composable @Composable

View File

@@ -35,7 +35,7 @@ import java.io.File
class RecordingResult( class RecordingResult(
val file: File, val file: File,
val mimeType: String, val mimeType: String,
val amplitudes: List<Int>, val amplitudes: List<Float>,
val duration: Int, val duration: Int,
) )
@@ -44,7 +44,7 @@ class VoiceMessageRecorder {
private var outputFile: File? = null private var outputFile: File? = null
private var startTime: Long = 0 private var startTime: Long = 0
private var job: Job? = null private var job: Job? = null
private var amplitudes: MutableList<Int> = mutableListOf() private var amplitudes: MutableList<Float> = mutableListOf()
private fun createRecorder(context: Context): MediaRecorder = private fun createRecorder(context: Context): MediaRecorder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -80,7 +80,7 @@ class VoiceMessageRecorder {
job = job =
scope.launch { scope.launch {
while (recorder != null) { while (recorder != null) {
amplitudes.add(((recorder?.maxAmplitude ?: 0) / 100).toInt()) amplitudes.add(recorder?.maxAmplitude?.toFloat() ?: 0f)
delay(1000) delay(1000)
} }
} }

View File

@@ -20,6 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.R.attr.maxHeight
import android.R.attr.minHeight
import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@@ -48,6 +50,7 @@ import androidx.compose.ui.unit.coerceIn
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.linc.audiowaveform.model.AmplitudeType import com.linc.audiowaveform.model.AmplitudeType
import com.linc.audiowaveform.model.WaveformAlignment import com.linc.audiowaveform.model.WaveformAlignment
import com.vitorpamplona.amethyst.ui.components.toDrawableAmplitudes
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -77,7 +80,7 @@ fun AudioWaveformReadOnly(
spikeRadius: Dp = 2.dp, spikeRadius: Dp = 2.dp,
spikePadding: Dp = 2.dp, spikePadding: Dp = 2.dp,
progress: Float = 0F, progress: Float = 0F,
amplitudes: List<Int>, amplitudes: List<Float>,
onProgressChange: (Float) -> Unit, onProgressChange: (Float) -> Unit,
) { ) {
val backgroundColor = MaterialTheme.colorScheme.background val backgroundColor = MaterialTheme.colorScheme.background
@@ -101,6 +104,7 @@ fun AudioWaveformReadOnly(
maxHeight = canvasSize.height.coerceAtLeast(MIN_SPIKE_HEIGHT), maxHeight = canvasSize.height.coerceAtLeast(MIN_SPIKE_HEIGHT),
) )
}.map { animateFloatAsState(it, spikeAnimationSpec).value } }.map { animateFloatAsState(it, spikeAnimationSpec).value }
Canvas( Canvas(
modifier = modifier =
Modifier Modifier
@@ -145,14 +149,13 @@ fun AudioWaveformReadOnly(
} }
} }
private fun List<Int>.toDrawableAmplitudes( private fun List<Float>.toDrawableAmplitudes(
amplitudeType: AmplitudeType, amplitudeType: AmplitudeType,
spikes: Int, spikes: Int,
minHeight: Float, minHeight: Float,
maxHeight: Float, maxHeight: Float,
): List<Float> { ): List<Float> {
val amplitudes = map(Int::toFloat) if (this.isEmpty() || spikes == 0) {
if (amplitudes.isEmpty() || spikes == 0) {
return List(spikes) { minHeight } return List(spikes) { minHeight }
} }
val transform = { data: List<Float> -> val transform = { data: List<Float> ->
@@ -161,11 +164,10 @@ private fun List<Int>.toDrawableAmplitudes(
AmplitudeType.Max -> data.max() AmplitudeType.Max -> data.max()
AmplitudeType.Min -> data.min() AmplitudeType.Min -> data.min()
}.toFloat() }.toFloat()
.coerceIn(minHeight, maxHeight)
} }
return when { return when {
spikes > amplitudes.count() -> amplitudes.fillToSize(spikes, transform) spikes > this.count() -> this.fillToSize(spikes, transform)
else -> amplitudes.chunkToSize(spikes, transform) else -> this.chunkToSize(spikes, transform)
}.normalize(minHeight, maxHeight) }.normalize(minHeight, maxHeight)
} }

View File

@@ -54,7 +54,7 @@ class AudioHeaderEvent(
description: String, description: String,
downloadUrl: String, downloadUrl: String,
streamUrl: String? = null, streamUrl: String? = null,
wavefront: List<Int>? = null, wavefront: List<Float>? = null,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AudioHeaderEvent>.() -> Unit = {}, initializer: TagArrayBuilder<AudioHeaderEvent>.() -> Unit = {},
) = eventTemplate(KIND, description, createdAt) { ) = eventTemplate(KIND, description, createdAt) {

View File

@@ -29,4 +29,4 @@ fun TagArrayBuilder<AudioHeaderEvent>.downloadUrl(downloadUrlTag: String) = addU
fun TagArrayBuilder<AudioHeaderEvent>.streamUrl(streamUrl: String) = addUnique(StreamUrlTag.assemble(streamUrl)) fun TagArrayBuilder<AudioHeaderEvent>.streamUrl(streamUrl: String) = addUnique(StreamUrlTag.assemble(streamUrl))
fun TagArrayBuilder<AudioHeaderEvent>.wavefront(wave: List<Int>) = addUnique(WaveformTag.assemble(wave)) fun TagArrayBuilder<AudioHeaderEvent>.wavefront(wave: List<Float>) = addUnique(WaveformTag.assemble(wave))

View File

@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.utils.ensure import com.vitorpamplona.quartz.utils.ensure
class WaveformTag( class WaveformTag(
val wave: List<Int>, val wave: List<Float>,
) { ) {
fun toTagArray() = assemble(wave) fun toTagArray() = assemble(wave)
@@ -38,12 +38,12 @@ class WaveformTag(
ensure(tag.has(1)) { return null } ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null } ensure(tag[1].isNotEmpty()) { return null }
val wave = runCatching { JsonMapper.mapper.readValue<List<Int>>(tag[1]) }.getOrNull() val wave = runCatching { JsonMapper.mapper.readValue<List<Float>>(tag[1]) }.getOrNull()
if (wave.isNullOrEmpty()) return null if (wave.isNullOrEmpty()) return null
return WaveformTag(wave) return WaveformTag(wave)
} }
@JvmStatic @JvmStatic
fun assemble(wave: List<Int>) = arrayOf(TAG_NAME, JsonMapper.mapper.writeValueAsString(wave)) fun assemble(wave: List<Float>) = arrayOf(TAG_NAME, JsonMapper.mapper.writeValueAsString(wave))
} }
} }

View File

@@ -29,7 +29,7 @@ data class AudioMeta(
val mimeType: String? = null, val mimeType: String? = null,
val hash: String? = null, val hash: String? = null,
val duration: Int? = null, val duration: Int? = null,
val waveform: List<Int>? = null, val waveform: List<Float>? = null,
) { ) {
fun toIMetaArray(): Array<String> = fun toIMetaArray(): Array<String> =
IMetaTagBuilder(url) IMetaTagBuilder(url)
@@ -37,7 +37,7 @@ data class AudioMeta(
mimeType?.let { mimeType(it) } mimeType?.let { mimeType(it) }
hash?.let { hash(it) } hash?.let { hash(it) }
duration?.let { duration(it) } duration?.let { duration(it) }
waveform?.let { waveform(it) } waveform?.let { waveformFloat(it) }
}.build() }.build()
.toTagArray() .toTagArray()

View File

@@ -34,6 +34,8 @@ fun IMetaTagBuilder.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash)
fun IMetaTagBuilder.duration(size: Int) = add(DurationTag.TAG_NAME, size.toString()) fun IMetaTagBuilder.duration(size: Int) = add(DurationTag.TAG_NAME, size.toString())
fun IMetaTagBuilder.waveform(wave: List<Int>) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWave(wave)) fun IMetaTagBuilder.waveformInt(wave: List<Int>) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWaveInt(wave))
fun IMetaTagBuilder.waveformFloat(wave: List<Float>) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWaveFloat(wave))
fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime) fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime)

View File

@@ -33,7 +33,7 @@ fun <T : BaseVoiceEvent> TagArrayBuilder<T>.audioIMeta(
mimeType: String? = null, mimeType: String? = null,
hash: String? = null, hash: String? = null,
duration: Int? = null, duration: Int? = null,
waveform: List<Int>? = null, waveform: List<Float>? = null,
) = audioIMeta(AudioMeta(url, mimeType, hash, duration, waveform)) ) = audioIMeta(AudioMeta(url, mimeType, hash, duration, waveform))
fun <T : BaseVoiceEvent> TagArrayBuilder<T>.audioIMeta(imeta: AudioMeta): TagArrayBuilder<T> { fun <T : BaseVoiceEvent> TagArrayBuilder<T>.audioIMeta(imeta: AudioMeta): TagArrayBuilder<T> {

View File

@@ -43,7 +43,7 @@ class VoiceEvent(
mimeType: String?, mimeType: String?,
hash: String, hash: String,
duration: Int, duration: Int,
waveform: List<Int>, waveform: List<Float>,
) = build(AudioMeta(url, mimeType, hash, duration, waveform)) ) = build(AudioMeta(url, mimeType, hash, duration, waveform))
fun build( fun build(

View File

@@ -64,7 +64,7 @@ class VoiceReplyEvent(
mimeType: String?, mimeType: String?,
hash: String, hash: String,
duration: Int, duration: Int,
waveform: List<Int>, waveform: List<Float>,
replyingTo: EventHintBundle<VoiceEvent>, replyingTo: EventHintBundle<VoiceEvent>,
) = build(AudioMeta(url, mimeType, hash, duration, waveform), replyingTo) ) = build(AudioMeta(url, mimeType, hash, duration, waveform), replyingTo)

View File

@@ -22,11 +22,12 @@ package com.vitorpamplona.quartz.nipA0VoiceMessages.tags
import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure import com.vitorpamplona.quartz.utils.ensure
import kotlin.collections.joinToString
class WaveformTag( class WaveformTag(
val wave: List<Int>, val wave: List<Float>,
) { ) {
fun toTagArray() = assemble(wave) fun toTagArray() = assembleFloat(wave)
companion object { companion object {
const val TAG_NAME = "waveform" const val TAG_NAME = "waveform"
@@ -35,26 +36,32 @@ class WaveformTag(
fun parse(tag: Array<String>): WaveformTag? = parseWave(tag)?.let { WaveformTag(it) } fun parse(tag: Array<String>): WaveformTag? = parseWave(tag)?.let { WaveformTag(it) }
@JvmStatic @JvmStatic
fun parseWave(tag: Array<String>): List<Int>? { fun parseWave(tag: Array<String>): List<Float>? {
ensure(tag.has(1)) { return null } ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null } ensure(tag[1].isNotEmpty()) { return null }
val wave = tag[1].split(" ").mapNotNull { it.toIntOrNull() } val wave = tag[1].split(" ").mapNotNull { it.toFloatOrNull() }
if (wave.isEmpty()) return null if (wave.isEmpty()) return null
return wave return wave
} }
fun parseWave(wave: String): List<Int>? { fun parseWave(wave: String): List<Float>? {
val wave = wave.split(" ").mapNotNull { it.toIntOrNull() } val wave = wave.split(" ").mapNotNull { it.toFloatOrNull() }
if (wave.isEmpty()) return null if (wave.isEmpty()) return null
return wave return wave
} }
@JvmStatic @JvmStatic
fun assembleWave(wave: List<Int>) = wave.joinToString(" ") fun assembleWaveInt(wave: List<Int>) = wave.joinToString(" ")
@JvmStatic @JvmStatic
fun assemble(wave: List<Int>) = arrayOf(TAG_NAME, assembleWave(wave)) fun assembleInt(wave: List<Int>) = arrayOf(TAG_NAME, assembleWaveInt(wave))
@JvmStatic
fun assembleWaveFloat(wave: List<Float>) = wave.joinToString(" ")
@JvmStatic
fun assembleFloat(wave: List<Float>) = arrayOf(TAG_NAME, assembleWaveFloat(wave))
} }
} }