mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
feat: Add smooth scrolling to RealtimeAudioVisualizer
This commit is contained in:
parent
f0f20a6594
commit
d7500b52db
@ -11,6 +11,9 @@ import android.os.Handler
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
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.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -23,6 +26,7 @@ import com.arthenica.ffmpegkit.ReturnCode
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -44,6 +48,7 @@ class RecorderService: Service() {
|
|||||||
private var mediaRecorder: MediaRecorder? = null
|
private var mediaRecorder: MediaRecorder? = null
|
||||||
private var onError: MediaRecorder.OnErrorListener? = null
|
private var onError: MediaRecorder.OnErrorListener? = null
|
||||||
private var onStateChange: (RecorderState) -> Unit = {}
|
private var onStateChange: (RecorderState) -> Unit = {}
|
||||||
|
private var onAmplitudeUpdate: () -> Unit = {}
|
||||||
|
|
||||||
private var counter = 0
|
private var counter = 0
|
||||||
|
|
||||||
@ -79,19 +84,20 @@ class RecorderService: Service() {
|
|||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val progress: Float
|
override fun onDestroy() {
|
||||||
get() {
|
super.onDestroy()
|
||||||
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))
|
|
||||||
|
|
||||||
return duration / (settings.maxDuration / 1000f)
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnErrorListener(onError: MediaRecorder.OnErrorListener) {
|
fun setOnErrorListener(onError: MediaRecorder.OnErrorListener) {
|
||||||
this.onError = onError
|
this.onError = onError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOnAmplitudeUpdateListener(onAmplitudeUpdate: () -> Unit) {
|
||||||
|
this.onAmplitudeUpdate = onAmplitudeUpdate
|
||||||
|
}
|
||||||
|
|
||||||
fun setOnStateChangeListener(onStateChange: (RecorderState) -> Unit) {
|
fun setOnStateChangeListener(onStateChange: (RecorderState) -> Unit) {
|
||||||
this.onStateChange = onStateChange
|
this.onStateChange = onStateChange
|
||||||
}
|
}
|
||||||
@ -133,6 +139,7 @@ class RecorderService: Service() {
|
|||||||
val amplitude = mediaRecorder!!.maxAmplitude
|
val amplitude = mediaRecorder!!.maxAmplitude
|
||||||
amplitudes.add(amplitude)
|
amplitudes.add(amplitude)
|
||||||
|
|
||||||
|
onAmplitudeUpdate()
|
||||||
handler.postDelayed(::updateAmplitude, AMPLITUDE_UPDATE_INTERVAL)
|
handler.postDelayed(::updateAmplitude, AMPLITUDE_UPDATE_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ private const val MAX_AMPLITUDE = 10000
|
|||||||
@Composable
|
@Composable
|
||||||
fun AudioVisualizer(
|
fun AudioVisualizer(
|
||||||
amplitudes: List<Int>,
|
amplitudes: List<Int>,
|
||||||
showAll: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val primary = MaterialTheme.colorScheme.primary
|
val primary = MaterialTheme.colorScheme.primary
|
||||||
val primaryMuted = primary.copy(alpha = 0.3f)
|
val primaryMuted = primary.copy(alpha = 0.3f)
|
||||||
@ -36,17 +35,15 @@ fun AudioVisualizer(
|
|||||||
translate(width, height) {
|
translate(width, height) {
|
||||||
amplitudes.forEachIndexed { index, amplitude ->
|
amplitudes.forEachIndexed { index, amplitude ->
|
||||||
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
|
val amplitudePercentage = (amplitude.toFloat() / MAX_AMPLITUDE).coerceAtMost(1f)
|
||||||
val boxWidth = if (showAll) width / amplitudes.size else 15f
|
|
||||||
val boxHeight = height * amplitudePercentage
|
val boxHeight = height * amplitudePercentage
|
||||||
|
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = if (amplitudePercentage > 0.05f) primary else primaryMuted,
|
color = if (amplitudePercentage > 0.05f) primary else primaryMuted,
|
||||||
topLeft = Offset(
|
topLeft = Offset(
|
||||||
if (showAll) -width / amplitudes.size * index
|
-width / amplitudes.size * index,
|
||||||
else 30f * (index - amplitudes.size),
|
|
||||||
-boxHeight / 2f
|
-boxHeight / 2f
|
||||||
),
|
),
|
||||||
size = Size(boxWidth, boxHeight),
|
size = Size(width, boxHeight),
|
||||||
cornerRadius = CornerRadius(3f, 3f)
|
cornerRadius = CornerRadius(3f, 3f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ fun RecordingStatus(
|
|||||||
verticalArrangement = Arrangement.SpaceBetween,
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Box {}
|
Box {}
|
||||||
AudioVisualizer(amplitudes = service.amplitudes)
|
RealtimeAudioVisualizer(service = service)
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
|
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
|
||||||
|
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -23,6 +24,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
@ -50,6 +53,8 @@ fun StartRecording(
|
|||||||
if (service != null && service.amplitudes.isNotEmpty()) {
|
if (service != null && service.amplitudes.isNotEmpty()) {
|
||||||
Box {}
|
Box {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val primary = MaterialTheme.colorScheme.primary
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
RecorderService.startService(context, connection)
|
RecorderService.startService(context, connection)
|
||||||
@ -79,7 +84,7 @@ fun StartRecording(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (service != null && service.amplitudes.isNotEmpty()) {
|
if (service != null && service.amplitudes.isNotEmpty()) {
|
||||||
AudioVisualizer(amplitudes = service.amplitudes, showAll = true)
|
AudioVisualizer(amplitudes = service.amplitudes)
|
||||||
}
|
}
|
||||||
if (service?.originalRecordingStart != null)
|
if (service?.originalRecordingStart != null)
|
||||||
Button(
|
Button(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user