feat: Add smooth scrolling to RealtimeAudioVisualizer

This commit is contained in:
Myzel394 2023-08-05 17:28:27 +02:00
parent f0f20a6594
commit d7500b52db
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
5 changed files with 113 additions and 14 deletions

View File

@ -11,6 +11,9 @@ import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.core.app.NotificationCompat
@ -23,6 +26,7 @@ import com.arthenica.ffmpegkit.ReturnCode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.File
@ -44,6 +48,7 @@ class RecorderService: Service() {
private var mediaRecorder: MediaRecorder? = null
private var onError: MediaRecorder.OnErrorListener? = null
private var onStateChange: (RecorderState) -> Unit = {}
private var onAmplitudeUpdate: () -> Unit = {}
private var counter = 0
@ -79,19 +84,20 @@ class RecorderService: Service() {
return super.onStartCommand(intent, flags, startId)
}
val progress: Float
get() {
val start = recordingStart.value ?: return 0f
val now = LocalDateTime.now()
val duration = now.toEpochSecond(ZoneId.systemDefault().rules.getOffset(now)) - start.toEpochSecond(ZoneId.systemDefault().rules.getOffset(start))
override fun onDestroy() {
super.onDestroy()
return duration / (settings.maxDuration / 1000f)
}
scope.cancel()
}
fun setOnErrorListener(onError: MediaRecorder.OnErrorListener) {
this.onError = onError
}
fun setOnAmplitudeUpdateListener(onAmplitudeUpdate: () -> Unit) {
this.onAmplitudeUpdate = onAmplitudeUpdate
}
fun setOnStateChangeListener(onStateChange: (RecorderState) -> Unit) {
this.onStateChange = onStateChange
}
@ -133,6 +139,7 @@ class RecorderService: Service() {
val amplitude = mediaRecorder!!.maxAmplitude
amplitudes.add(amplitude)
onAmplitudeUpdate()
handler.postDelayed(::updateAmplitude, AMPLITUDE_UPDATE_INTERVAL)
}

View File

@ -20,7 +20,6 @@ private const val MAX_AMPLITUDE = 10000
@Composable
fun AudioVisualizer(
amplitudes: List<Int>,
showAll: Boolean = false
) {
val primary = MaterialTheme.colorScheme.primary
val primaryMuted = primary.copy(alpha = 0.3f)
@ -36,17 +35,15 @@ fun AudioVisualizer(
translate(width, height) {
amplitudes.forEachIndexed { index, amplitude ->
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
val boxWidth = if (showAll) width / amplitudes.size else 15f
val boxHeight = height * amplitudePercentage
drawRoundRect(
color = if (amplitudePercentage > 0.05f) primary else primaryMuted,
topLeft = Offset(
if (showAll) -width / amplitudes.size * index
else 30f * (index - amplitudes.size),
-width / amplitudes.size * index,
-boxHeight / 2f
),
size = Size(boxWidth, boxHeight),
size = Size(width, boxHeight),
cornerRadius = CornerRadius(3f, 3f)
)
}

View File

@ -0,0 +1,90 @@
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.unit.dp
import app.myzel394.locationtest.services.RecorderService
import kotlinx.coroutines.launch
private const val MAX_AMPLITUDE = 10000
private const val BOX_WIDTH = 15f
private const val BOX_GAP = 15f
@Composable
fun RealtimeAudioVisualizer(
service: RecorderService,
) {
val scope = rememberCoroutineScope()
val amplitudes = service.amplitudes
val primary = MaterialTheme.colorScheme.primary
val primaryMuted = primary.copy(alpha = 0.3f)
// Moves the visualizer to the left
// A new amplitude is added every 100L milliseconds, so the visualizer moves one
// box width + gap in 100L
val animationProgress = remember { Animatable(0f) }
LaunchedEffect(Unit) {
service.setOnAmplitudeUpdateListener {
scope.launch {
animationProgress.snapTo(0f)
animationProgress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = 100,
easing = LinearEasing
)
)
}
}
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
) {
val height = this.size.height / 2f
val width = this.size.width
translate(width, height) {
translate(-animationProgress.value * (BOX_WIDTH + BOX_GAP), 0f) {
amplitudes.forEachIndexed { index, amplitude ->
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
val boxHeight = height * amplitudePercentage
drawRoundRect(
color = if (amplitudePercentage > 0.05f) primary else primaryMuted,
topLeft = Offset(
(BOX_WIDTH + BOX_GAP) * (index - amplitudes.size),
-boxHeight / 2f
),
size = Size(BOX_WIDTH, boxHeight),
cornerRadius = CornerRadius(3f, 3f)
)
}
}
}
}
}

View File

@ -68,7 +68,7 @@ fun RecordingStatus(
verticalArrangement = Arrangement.SpaceBetween,
) {
Box {}
AudioVisualizer(amplitudes = service.amplitudes)
RealtimeAudioVisualizer(service = service)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {

View File

@ -1,6 +1,7 @@
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
import android.content.ServiceConnection
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -23,6 +24,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@ -50,6 +53,8 @@ fun StartRecording(
if (service != null && service.amplitudes.isNotEmpty()) {
Box {}
}
val primary = MaterialTheme.colorScheme.primary
Button(
onClick = {
RecorderService.startService(context, connection)
@ -79,7 +84,7 @@ fun StartRecording(
}
}
if (service != null && service.amplitudes.isNotEmpty()) {
AudioVisualizer(amplitudes = service.amplitudes, showAll = true)
AudioVisualizer(amplitudes = service.amplitudes)
}
if (service?.originalRecordingStart != null)
Button(