diff --git a/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt b/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt index 6338d1b..6cd4173 100644 --- a/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt @@ -11,6 +11,7 @@ import android.os.Handler import android.os.IBinder import android.os.Looper import android.util.Log +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat @@ -32,6 +33,8 @@ import java.util.Date import java.util.UUID; +const val AMPLITUDE_UPDATE_INTERVAL = 100L + class RecorderService: Service() { private val binder = LocalBinder() private val handler = Handler(Looper.getMainLooper()) @@ -56,6 +59,7 @@ class RecorderService: Service() { get() = recordingStart.value != null val filePaths = mutableListOf() + val amplitudes = mutableStateListOf() var originalRecordingStart: LocalDateTime? = null private set @@ -121,6 +125,17 @@ class RecorderService: Service() { return File(outputFile) } + private fun updateAmplitude() { + if (!isRecording || mediaRecorder == null) { + return + } + + val amplitude = mediaRecorder!!.maxAmplitude + amplitudes.add(amplitude) + + handler.postDelayed(::updateAmplitude, AMPLITUDE_UPDATE_INTERVAL) + } + private fun startNewRecording() { if (!isRecording) { return @@ -183,6 +198,7 @@ class RecorderService: Service() { private fun start() { + amplitudes.clear() filePaths.clear() // Create folder File(this.fileFolder!!).mkdirs() @@ -196,6 +212,7 @@ class RecorderService: Service() { showNotification() startNewRecording() + updateAmplitude() } } } diff --git a/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/AudioVisualizer.kt b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/AudioVisualizer.kt new file mode 100644 index 0000000..5415e0d --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/AudioVisualizer.kt @@ -0,0 +1,48 @@ +package app.myzel394.locationtest.ui.components.AudioRecorder.atoms + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +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 + +// Inspired by https://github.com/Bnyro/RecordYou/blob/main/app/src/main/java/com/bnyro/recorder/ui/components/AudioVisualizer.kt + +private const val MAX_AMPLITUDE = 10000 + +@Composable +fun AudioVisualizer( + amplitudes: List, +) { + val primary = MaterialTheme.colorScheme.primary + val primaryMuted = primary.copy(alpha = 0.3f) + + Canvas( + modifier = Modifier.width(300.dp).height(300.dp) + ) { + val height = this.size.height / 2f + val width = this.size.width + + translate(width, height) { + 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( + 30f * (index - amplitudes.size), + -boxHeight / 2f + ), + size = Size(15f, boxHeight), + cornerRadius = CornerRadius(3f, 3f) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt index 84229a0..623b36f 100644 --- a/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt +++ b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt @@ -1,12 +1,5 @@ package app.myzel394.locationtest.ui.components.AudioRecorder.atoms -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -27,10 +20,13 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -39,38 +35,43 @@ import app.myzel394.locationtest.services.RecorderService import app.myzel394.locationtest.ui.components.atoms.Pulsating import app.myzel394.locationtest.ui.utils.formatDuration import app.myzel394.locationtest.ui.utils.rememberFileSaverDialog +import kotlinx.coroutines.delay import java.time.Duration import java.time.LocalDateTime +import java.time.ZoneId @Composable fun RecordingStatus( service: RecorderService, ) { val context = LocalContext.current - val saveFile = rememberFileSaverDialog("audio/*") + var now by remember { mutableStateOf(LocalDateTime.now()) } + + val start = service.recordingStart.value!! + val duration = now.toEpochSecond(ZoneId.systemDefault().rules.getOffset(now)) - start.toEpochSecond(ZoneId.systemDefault().rules.getOffset(start)) + val progress = duration / (service.settings.maxDuration / 1000f) + + LaunchedEffect(Unit) { + while (true) { + now = LocalDateTime.now() + delay(1000) + } + } + Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - // Forces real time update for the text - val transition = rememberInfiniteTransition() - val forceUpdateValue by transition.animateFloat( - initialValue = .999999f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween(1000), - repeatMode = RepeatMode.Reverse - ) - ) + AudioVisualizer(amplitudes = service.amplitudes) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { - val distance = Duration.between(service.recordingStart.value, LocalDateTime.now()).toMillis() + val distance = Duration.between(service.recordingStart.value, now).toMillis() Pulsating { Box( @@ -84,15 +85,13 @@ fun RecordingStatus( Text( text = formatDuration(distance), style = MaterialTheme.typography.headlineLarge, - modifier = Modifier.alpha(forceUpdateValue) ) } Spacer(modifier = Modifier.height(8.dp)) LinearProgressIndicator( - progress = service.progress, + progress = progress, modifier = Modifier .width(300.dp) - .alpha(forceUpdateValue) ) Spacer(modifier = Modifier.height(32.dp)) Button(