mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
WIP visualizer
This commit is contained in:
parent
11587c5fcf
commit
74f935e1f6
4 changed files with 123 additions and 6 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
import android.media.audiofx.Visualizer
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
|
|
@ -43,6 +44,8 @@ import timber.log.Timber
|
|||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Singleton
|
||||
|
|
@ -54,10 +57,12 @@ class MusicService
|
|||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val imageUrlService: ImageUrlService,
|
||||
) {
|
||||
) : Visualizer.OnDataCaptureListener {
|
||||
private val _state = MutableStateFlow(MusicServiceState.EMPTY)
|
||||
val state: StateFlow<MusicServiceState> = _state
|
||||
|
||||
val viz = MutableStateFlow<IntArray>(IntArray(0))
|
||||
|
||||
val player: Player by lazy {
|
||||
ExoPlayer
|
||||
.Builder(context)
|
||||
|
|
@ -71,6 +76,12 @@ class MusicService
|
|||
}
|
||||
}
|
||||
|
||||
private val visualizer by lazy {
|
||||
Visualizer(player.audioSessionId).apply {
|
||||
captureSize = Visualizer.getCaptureSizeRange()[1]
|
||||
}
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
var mediaSession: MediaSession? = null
|
||||
private set
|
||||
|
|
@ -81,6 +92,15 @@ class MusicService
|
|||
if (mediaSession == null) {
|
||||
Timber.i("Starting music MediaSession")
|
||||
mediaSession = MediaSession.Builder(context, player).build()
|
||||
onMain {
|
||||
visualizer.setDataCaptureListener(
|
||||
this@MusicService,
|
||||
Visualizer.getMaxCaptureRate() / 2,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
visualizer.enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +122,7 @@ class MusicService
|
|||
player.stop()
|
||||
player.setMediaItems(emptyList())
|
||||
}
|
||||
visualizer.enabled = false
|
||||
_state.update {
|
||||
MusicServiceState.EMPTY
|
||||
}
|
||||
|
|
@ -287,6 +308,37 @@ class MusicService
|
|||
_state.update { it.copy(loadingState = LoadingState.Success) }
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onFftDataCapture(
|
||||
visualizer: Visualizer,
|
||||
fft: ByteArray,
|
||||
samplingRate: Int,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onWaveFormDataCapture(
|
||||
visualizer: Visualizer,
|
||||
waveform: ByteArray,
|
||||
samplingRate: Int,
|
||||
) {
|
||||
val resolution = 64
|
||||
val processed = IntArray(resolution)
|
||||
val captureSize =
|
||||
Visualizer.getCaptureSizeRange()[1]
|
||||
val groupSize = captureSize / resolution
|
||||
// val groupSize = captureSize / resolution.toFloat()
|
||||
for (i in 0 until resolution) {
|
||||
processed[i] =
|
||||
waveform
|
||||
.map { abs(it.toInt()) }
|
||||
// .map { it.toInt() }
|
||||
.subList(i * groupSize, min((i + 1) * groupSize, waveform.size))
|
||||
.average()
|
||||
.toInt()
|
||||
// processed[i] = abs(waveform[ceil(i * finder).toInt()].toInt())
|
||||
}
|
||||
viz.update { processed }
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.music
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
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.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
|
||||
@Composable
|
||||
fun BoxScope.BarVisualizer(
|
||||
data: IntArray,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.onSizeChanged { size = it },
|
||||
) {
|
||||
val size = with(LocalDensity.current) { DpSize(size.width.toDp(), size.height.toDp()) }
|
||||
val padding = 1.dp
|
||||
val width = size.width / data.size
|
||||
|
||||
data.forEachIndexed { index, data ->
|
||||
val height by animateDpAsState(
|
||||
targetValue = size.height * data / 128f / 1.5f,
|
||||
animationSpec = tween(easing = LinearEasing),
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.height(height)
|
||||
.width(width)
|
||||
.padding(start = if (index == 0) 0.dp else padding)
|
||||
.background(MaterialTheme.colorScheme.border),
|
||||
// .align(Alignment.Bottom),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ fun NowPlayingPage(
|
|||
state.musicServiceState.queueSize,
|
||||
)
|
||||
val current = queue.getOrNull(state.musicServiceState.currentIndex)
|
||||
val viz by viewModel.viz.collectAsState()
|
||||
|
||||
val controllerViewState = viewModel.controllerViewState
|
||||
val preferences =
|
||||
|
|
@ -165,11 +166,17 @@ fun NowPlayingPage(
|
|||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(.7f)) {
|
||||
AsyncImage(
|
||||
contentDescription = null,
|
||||
model = current?.imageUrl,
|
||||
modifier = Modifier.fillMaxSize(.7f),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
BarVisualizer(
|
||||
data = viz,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
current?.title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ class NowPlayingViewModel
|
|||
val state = MutableStateFlow(NowPlayingState(musicService.state.value))
|
||||
val player get() = musicService.player
|
||||
|
||||
val viz = musicService.viz
|
||||
|
||||
private val lyricCache =
|
||||
InMemoryKache<UUID, LyricDto>(20) {
|
||||
creationScope = CoroutineScope(Dispatchers.IO)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue