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
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.audiofx.Visualizer
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
|
@ -43,6 +44,8 @@ import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
@ -54,10 +57,12 @@ class MusicService
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val imageUrlService: ImageUrlService,
|
private val imageUrlService: ImageUrlService,
|
||||||
) {
|
) : Visualizer.OnDataCaptureListener {
|
||||||
private val _state = MutableStateFlow(MusicServiceState.EMPTY)
|
private val _state = MutableStateFlow(MusicServiceState.EMPTY)
|
||||||
val state: StateFlow<MusicServiceState> = _state
|
val state: StateFlow<MusicServiceState> = _state
|
||||||
|
|
||||||
|
val viz = MutableStateFlow<IntArray>(IntArray(0))
|
||||||
|
|
||||||
val player: Player by lazy {
|
val player: Player by lazy {
|
||||||
ExoPlayer
|
ExoPlayer
|
||||||
.Builder(context)
|
.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()
|
private val mutex = Mutex()
|
||||||
var mediaSession: MediaSession? = null
|
var mediaSession: MediaSession? = null
|
||||||
private set
|
private set
|
||||||
|
|
@ -81,6 +92,15 @@ class MusicService
|
||||||
if (mediaSession == null) {
|
if (mediaSession == null) {
|
||||||
Timber.i("Starting music MediaSession")
|
Timber.i("Starting music MediaSession")
|
||||||
mediaSession = MediaSession.Builder(context, player).build()
|
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.stop()
|
||||||
player.setMediaItems(emptyList())
|
player.setMediaItems(emptyList())
|
||||||
}
|
}
|
||||||
|
visualizer.enabled = false
|
||||||
_state.update {
|
_state.update {
|
||||||
MusicServiceState.EMPTY
|
MusicServiceState.EMPTY
|
||||||
}
|
}
|
||||||
|
|
@ -287,6 +308,37 @@ class MusicService
|
||||||
_state.update { it.copy(loadingState = LoadingState.Success) }
|
_state.update { it.copy(loadingState = LoadingState.Success) }
|
||||||
return result
|
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
|
@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,
|
state.musicServiceState.queueSize,
|
||||||
)
|
)
|
||||||
val current = queue.getOrNull(state.musicServiceState.currentIndex)
|
val current = queue.getOrNull(state.musicServiceState.currentIndex)
|
||||||
|
val viz by viewModel.viz.collectAsState()
|
||||||
|
|
||||||
val controllerViewState = viewModel.controllerViewState
|
val controllerViewState = viewModel.controllerViewState
|
||||||
val preferences =
|
val preferences =
|
||||||
|
|
@ -165,11 +166,17 @@ fun NowPlayingPage(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
Box(modifier = Modifier.fillMaxSize(.7f)) {
|
||||||
contentDescription = null,
|
AsyncImage(
|
||||||
model = current?.imageUrl,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxSize(.7f),
|
model = current?.imageUrl,
|
||||||
)
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
BarVisualizer(
|
||||||
|
data = viz,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
current?.title?.let {
|
current?.title?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ class NowPlayingViewModel
|
||||||
val state = MutableStateFlow(NowPlayingState(musicService.state.value))
|
val state = MutableStateFlow(NowPlayingState(musicService.state.value))
|
||||||
val player get() = musicService.player
|
val player get() = musicService.player
|
||||||
|
|
||||||
|
val viz = musicService.viz
|
||||||
|
|
||||||
private val lyricCache =
|
private val lyricCache =
|
||||||
InMemoryKache<UUID, LyricDto>(20) {
|
InMemoryKache<UUID, LyricDto>(20) {
|
||||||
creationScope = CoroutineScope(Dispatchers.IO)
|
creationScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue