mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
feat: Add AudioVisualizer
This commit is contained in:
parent
c8a02567ab
commit
9b1a439657
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user