More visualizer

This commit is contained in:
Damontecres 2026-03-04 19:45:28 -05:00
parent 74f935e1f6
commit 8053f5397f
No known key found for this signature in database
5 changed files with 69 additions and 63 deletions

View file

@ -1,7 +1,6 @@
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
@ -44,8 +43,6 @@ 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
@ -57,12 +54,10 @@ 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)
@ -76,12 +71,6 @@ 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
@ -92,15 +81,6 @@ 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
}
}
}
}
@ -122,7 +102,6 @@ class MusicService
player.stop()
player.setMediaItems(emptyList())
}
visualizer.enabled = false
_state.update {
MusicServiceState.EMPTY
}
@ -308,37 +287,6 @@ 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

View file

@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.MediaManagementService
import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SuggestionService
@ -62,12 +63,14 @@ class RecommendedMusicViewModel
favoriteWatchManager: FavoriteWatchManager,
mediaReportService: MediaReportService,
backdropService: BackdropService,
mediaManagementService: MediaManagementService,
) : RecommendedViewModel(
context,
navigationManager,
favoriteWatchManager,
mediaReportService,
backdropService,
mediaManagementService,
) {
@AssistedFactory
interface Factory {

View file

@ -40,7 +40,7 @@ fun BoxScope.BarVisualizer(
data.forEachIndexed { index, data ->
val height by animateDpAsState(
targetValue = size.height * data / 128f / 1.5f,
targetValue = size.height * data / 128f,
animationSpec = tween(easing = LinearEasing),
)
Box(

View file

@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@ -166,12 +167,19 @@ fun NowPlayingPage(
.fillMaxSize()
.weight(1f),
) {
Box(modifier = Modifier.fillMaxSize(.7f)) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight(.75f),
) {
AsyncImage(
contentDescription = null,
model = current?.imageUrl,
modifier = Modifier.fillMaxSize(),
)
BarVisualizer(
data = viz,
modifier = Modifier.fillMaxSize(),

View file

@ -1,15 +1,13 @@
package com.github.damontecres.wholphin.ui.detail.music
import android.content.Context
import android.media.audiofx.Visualizer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.MusicService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.UserPreferencesService
@ -35,6 +33,8 @@ import org.jellyfin.sdk.model.api.LyricDto
import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
import java.util.UUID
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@ -45,18 +45,22 @@ class NowPlayingViewModel
private val api: ApiClient,
@param:ApplicationContext private val context: Context,
val navigationManager: NavigationManager,
private val favoriteWatchManager: FavoriteWatchManager,
private val backdropService: BackdropService,
private val imageUrlService: ImageUrlService,
private val musicService: MusicService,
val userPreferencesService: UserPreferencesService,
) : ViewModel(),
Visualizer.OnDataCaptureListener,
Player.Listener {
@AssistedFactory
interface Factory {
fun create(): NowPlayingViewModel
}
private val visualizer by lazy {
Visualizer(player.audioSessionId).apply {
captureSize = Visualizer.getCaptureSizeRange()[1]
}
}
val controllerViewState =
ControllerViewState(
AppPreference.Companion.ControllerTimeout.defaultValue,
@ -66,7 +70,7 @@ class NowPlayingViewModel
val state = MutableStateFlow(NowPlayingState(musicService.state.value))
val player get() = musicService.player
val viz = musicService.viz
val viz = MutableStateFlow<IntArray>(IntArray(0))
private val lyricCache =
InMemoryKache<UUID, LyricDto>(20) {
@ -75,7 +79,18 @@ class NowPlayingViewModel
init {
player.addListener(this)
addCloseable { player.removeListener(this) }
visualizer.setDataCaptureListener(
this,
Visualizer.getMaxCaptureRate() / 2,
true,
false,
)
visualizer.enabled = true
addCloseable {
player.removeListener(this)
visualizer.release()
}
viewModelScope.launchDefault {
musicService.state.collectLatest { musicServiceState ->
state.update { it.copy(musicServiceState = musicServiceState) }
@ -192,4 +207,36 @@ class NowPlayingViewModel
navigationManager.goBack()
}
}
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()
val v = abs(waveform[ceil(i * groupSize).toInt()].toInt())
processed[i] = v * v / (128)
}
viz.update { processed }
}
}