mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
More visualizer
This commit is contained in:
parent
74f935e1f6
commit
8053f5397f
5 changed files with 69 additions and 63 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
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
|
||||||
|
|
@ -44,8 +43,6 @@ 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
|
||||||
|
|
@ -57,12 +54,10 @@ 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)
|
||||||
|
|
@ -76,12 +71,6 @@ 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
|
||||||
|
|
@ -92,15 +81,6 @@ 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +102,6 @@ class MusicService
|
||||||
player.stop()
|
player.stop()
|
||||||
player.setMediaItems(emptyList())
|
player.setMediaItems(emptyList())
|
||||||
}
|
}
|
||||||
visualizer.enabled = false
|
|
||||||
_state.update {
|
_state.update {
|
||||||
MusicServiceState.EMPTY
|
MusicServiceState.EMPTY
|
||||||
}
|
}
|
||||||
|
|
@ -308,37 +287,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
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.MediaReportService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SuggestionService
|
import com.github.damontecres.wholphin.services.SuggestionService
|
||||||
|
|
@ -62,12 +63,14 @@ class RecommendedMusicViewModel
|
||||||
favoriteWatchManager: FavoriteWatchManager,
|
favoriteWatchManager: FavoriteWatchManager,
|
||||||
mediaReportService: MediaReportService,
|
mediaReportService: MediaReportService,
|
||||||
backdropService: BackdropService,
|
backdropService: BackdropService,
|
||||||
|
mediaManagementService: MediaManagementService,
|
||||||
) : RecommendedViewModel(
|
) : RecommendedViewModel(
|
||||||
context,
|
context,
|
||||||
navigationManager,
|
navigationManager,
|
||||||
favoriteWatchManager,
|
favoriteWatchManager,
|
||||||
mediaReportService,
|
mediaReportService,
|
||||||
backdropService,
|
backdropService,
|
||||||
|
mediaManagementService,
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ fun BoxScope.BarVisualizer(
|
||||||
|
|
||||||
data.forEachIndexed { index, data ->
|
data.forEachIndexed { index, data ->
|
||||||
val height by animateDpAsState(
|
val height by animateDpAsState(
|
||||||
targetValue = size.height * data / 128f / 1.5f,
|
targetValue = size.height * data / 128f,
|
||||||
animationSpec = tween(easing = LinearEasing),
|
animationSpec = tween(easing = LinearEasing),
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -166,12 +167,19 @@ fun NowPlayingPage(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize(.7f)) {
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(.75f),
|
||||||
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
model = current?.imageUrl,
|
model = current?.imageUrl,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
BarVisualizer(
|
BarVisualizer(
|
||||||
data = viz,
|
data = viz,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.music
|
package com.github.damontecres.wholphin.ui.detail.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.audiofx.Visualizer
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
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.MusicService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
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 org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.ceil
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
@ -45,18 +45,22 @@ class NowPlayingViewModel
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
|
||||||
private val backdropService: BackdropService,
|
|
||||||
private val imageUrlService: ImageUrlService,
|
|
||||||
private val musicService: MusicService,
|
private val musicService: MusicService,
|
||||||
val userPreferencesService: UserPreferencesService,
|
val userPreferencesService: UserPreferencesService,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
|
Visualizer.OnDataCaptureListener,
|
||||||
Player.Listener {
|
Player.Listener {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(): NowPlayingViewModel
|
fun create(): NowPlayingViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val visualizer by lazy {
|
||||||
|
Visualizer(player.audioSessionId).apply {
|
||||||
|
captureSize = Visualizer.getCaptureSizeRange()[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val controllerViewState =
|
val controllerViewState =
|
||||||
ControllerViewState(
|
ControllerViewState(
|
||||||
AppPreference.Companion.ControllerTimeout.defaultValue,
|
AppPreference.Companion.ControllerTimeout.defaultValue,
|
||||||
|
|
@ -66,7 +70,7 @@ 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
|
val viz = MutableStateFlow<IntArray>(IntArray(0))
|
||||||
|
|
||||||
private val lyricCache =
|
private val lyricCache =
|
||||||
InMemoryKache<UUID, LyricDto>(20) {
|
InMemoryKache<UUID, LyricDto>(20) {
|
||||||
|
|
@ -75,7 +79,18 @@ class NowPlayingViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
player.addListener(this)
|
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 {
|
viewModelScope.launchDefault {
|
||||||
musicService.state.collectLatest { musicServiceState ->
|
musicService.state.collectLatest { musicServiceState ->
|
||||||
state.update { it.copy(musicServiceState = musicServiceState) }
|
state.update { it.copy(musicServiceState = musicServiceState) }
|
||||||
|
|
@ -192,4 +207,36 @@ class NowPlayingViewModel
|
||||||
navigationManager.goBack()
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue