feat: Add AudioVisualizer

This commit is contained in:
Myzel394 2023-08-05 15:38:11 +02:00
parent c8a02567ab
commit 9b1a439657
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
3 changed files with 87 additions and 23 deletions

View File

@ -11,6 +11,7 @@ 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.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -32,6 +33,8 @@ import java.util.Date
import java.util.UUID; import java.util.UUID;
const val AMPLITUDE_UPDATE_INTERVAL = 100L
class RecorderService: Service() { class RecorderService: Service() {
private val binder = LocalBinder() private val binder = LocalBinder()
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@ -56,6 +59,7 @@ class RecorderService: Service() {
get() = recordingStart.value != null get() = recordingStart.value != null
val filePaths = mutableListOf<String>() val filePaths = mutableListOf<String>()
val amplitudes = mutableStateListOf<Int>()
var originalRecordingStart: LocalDateTime? = null var originalRecordingStart: LocalDateTime? = null
private set private set
@ -121,6 +125,17 @@ class RecorderService: Service() {
return File(outputFile) 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() { private fun startNewRecording() {
if (!isRecording) { if (!isRecording) {
return return
@ -183,6 +198,7 @@ class RecorderService: Service() {
private fun start() { private fun start() {
amplitudes.clear()
filePaths.clear() filePaths.clear()
// Create folder // Create folder
File(this.fileFolder!!).mkdirs() File(this.fileFolder!!).mkdirs()
@ -196,6 +212,7 @@ class RecorderService: Service() {
showNotification() showNotification()
startNewRecording() startNewRecording()
updateAmplitude()
} }
} }
} }

View File

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

View File

@ -1,12 +1,5 @@
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -27,10 +20,13 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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.components.atoms.Pulsating
import app.myzel394.locationtest.ui.utils.formatDuration import app.myzel394.locationtest.ui.utils.formatDuration
import app.myzel394.locationtest.ui.utils.rememberFileSaverDialog import app.myzel394.locationtest.ui.utils.rememberFileSaverDialog
import kotlinx.coroutines.delay
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId
@Composable @Composable
fun RecordingStatus( fun RecordingStatus(
service: RecorderService, service: RecorderService,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val saveFile = rememberFileSaverDialog("audio/*") 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( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
// Forces real time update for the text AudioVisualizer(amplitudes = service.amplitudes)
val transition = rememberInfiniteTransition()
val forceUpdateValue by transition.animateFloat(
initialValue = .999999f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
val distance = Duration.between(service.recordingStart.value, LocalDateTime.now()).toMillis() val distance = Duration.between(service.recordingStart.value, now).toMillis()
Pulsating { Pulsating {
Box( Box(
@ -84,15 +85,13 @@ fun RecordingStatus(
Text( Text(
text = formatDuration(distance), text = formatDuration(distance),
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.alpha(forceUpdateValue)
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator( LinearProgressIndicator(
progress = service.progress, progress = progress,
modifier = Modifier modifier = Modifier
.width(300.dp) .width(300.dp)
.alpha(forceUpdateValue)
) )
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
Button( Button(