Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,6 @@ dependencies {

// 取主题色
implementation(libs.androidx.palette.ktx)

implementation(libs.superlyricapi)
}
7 changes: 7 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,10 @@
-dontwarn java.beans.**
-dontwarn javax.script.**
-dontwarn org.mozilla.javascript.**

# 防止编译及运行错误
-keep class com.hchen.superlyricapi.** { *;}
-dontwarn android.os.ServiceManager

# 避免 kotlin.reflect 内部反射崩溃
-keepattributes *Annotation*
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package moe.ouom.neriplayer.core.lyricon

import android.content.Context
import com.hchen.superlyricapi.SuperLyricData
import com.hchen.superlyricapi.SuperLyricHelper
import com.hchen.superlyricapi.SuperLyricLine
import com.hchen.superlyricapi.SuperLyricWord
import io.github.proify.lyricon.provider.LyriconFactory
import io.github.proify.lyricon.provider.LyriconProvider
import io.github.proify.lyricon.lyric.model.LyricWord
Expand All @@ -10,15 +14,21 @@ import io.github.proify.lyricon.provider.service.addConnectionListener
import moe.ouom.neriplayer.ui.component.LyricEntry
import moe.ouom.neriplayer.ui.viewmodel.playlist.SongItem
import moe.ouom.neriplayer.util.NPLogger
import kotlin.collections.getOrNull

object LyriconManager {
private var provider: LyriconProvider? = null
@Volatile
private var enabled: Boolean = false
private var lastLyricIndex: Int = -1
private var lyrics : List<LyricEntry>? = null
private var translatedLyrics :List<LyricEntry>? = null
private var currentSong: SongItem? = null

fun initialize(context: Context) {
if (provider != null) return
try {
if (SuperLyricHelper.isAvailable()) { SuperLyricHelper.registerPublisher(); }
provider = LyriconFactory.createProvider(context)
provider?.register()

Expand All @@ -28,6 +38,7 @@ object LyriconManager {
onDisconnected { NPLogger.d("LyriconManager", "Disconnected") }
onConnectTimeout { NPLogger.d("LyriconManager", "ConnectTimeout") }
}

} catch (e: Exception) {
NPLogger.e("LyriconManager", "Failed to initialize LyriconProvider", e)
}
Expand Down Expand Up @@ -55,6 +66,7 @@ object LyriconManager {
if (!enabled) return
try {
provider?.player?.setPosition(positionMs)
updateSuperLyric(positionMs)
} catch (e: Exception) {
// NPLogger.e("LyriconManager", "setPosition failed", e)
}
Expand All @@ -64,6 +76,9 @@ object LyriconManager {
if (!enabled) return
try {
val translationToleranceMs = 1_500L
LyriconManager.lyrics = lyrics
LyriconManager.translatedLyrics = translatedLyrics
currentSong = song
val lyriconLyrics = lyrics?.map { entry ->
val words = if (entry.words != null) {
var currentIndex = 0
Expand Down Expand Up @@ -110,4 +125,79 @@ object LyriconManager {
NPLogger.e("LyriconManager", "updateSong failed", e)
}
}


private fun updateSuperLyric(positionMs: Long) {

try {
if (!SuperLyricHelper.isAvailable()) return

val lyricList = lyrics ?: return
val song = currentSong ?: return

val index = lyricList.indexOfLast {
it.startTimeMs <= positionMs
}

// 没有匹配歌词
if (index < 0) return

// 避免重复发送
if (index == lastLyricIndex) return

lastLyricIndex = index

val line = lyricList.getOrNull(index) ?: return


val translation = translatedLyrics
?.firstOrNull {
kotlin.math.abs(
it.startTimeMs - line.startTimeMs
) <= 1500
}

var currentIndex = 0
val words = line.words
?.mapNotNull { wordTiming ->
if (currentIndex + wordTiming.charCount <= line.text.length) {
val wordText =
line.text.substring(currentIndex, currentIndex + wordTiming.charCount)
currentIndex += wordTiming.charCount
SuperLyricWord(
wordText,
wordTiming.startTimeMs,
wordTiming.endTimeMs
)
} else {
null
}
}
?: emptyList()


val data = SuperLyricData()
.setTitle(song.name)
.setArtist(song.artist)
.setAlbum(song.album)
.setLyric(
SuperLyricLine(
line.text,
words.toTypedArray(),
line.startTimeMs,
line.endTimeMs
)
)
.setTranslation(
SuperLyricLine(
translation?.text ?: ""
)
)


SuperLyricHelper.sendLyric(data)
} catch (e: Exception) {
NPLogger.e("LyriconManager", "updateSuperLyric failed", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.ouom.neriplayer.R
import moe.ouom.neriplayer.activity.MainActivity
import moe.ouom.neriplayer.core.player.PlayerManager.externalBluetoothLyricLineFlow
import moe.ouom.neriplayer.core.player.PlayerManager.externalBluetoothLyrics
import moe.ouom.neriplayer.core.player.PlayerManager.statusBarLyricsEnable
import moe.ouom.neriplayer.core.player.metadata.resolveExternalBluetoothMetadataText
import moe.ouom.neriplayer.core.player.metadata.shouldUseExternalBluetoothLyrics
import moe.ouom.neriplayer.data.local.media.LocalSongSupport
Expand Down Expand Up @@ -513,6 +519,13 @@ class AudioPlayerService : Service() {
}
}

externalBluetoothLyricLineFlow
.filterNotNull()
.onEach { lyric ->
if (statusBarLyricsEnable && PlayerManager.handleAudioBecomingNoisy()) { updateNotification() }
}
.launchIn(serviceScope)

becomingNoisyReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
Expand Down Expand Up @@ -731,6 +744,12 @@ class AudioPlayerService : Service() {
builder.addAction(R.drawable.round_skip_next_24, getString(R.string.player_next), nextIntent)

builder.setContentTitle(song?.displayName() ?: "NeriPlayer")
//魅族状态栏歌词设置这个
if (statusBarLyricsEnable) {
if (externalBluetoothLyricLineFlow.value != "" && externalBluetoothLyricLineFlow.value != null && externalBluetoothLyricLineFlow.value != "null") {
builder.setTicker(externalBluetoothLyricLineFlow.value.toString())
}
}

val timerState = PlayerManager.sleepTimerManager.timerState.value
val contentText = if (timerState.isActive) {
Expand All @@ -753,7 +772,17 @@ class AudioPlayerService : Service() {

currentLargeIcon?.let { builder.setLargeIcon(it) }

return builder.build()
return builder.build().apply {
if (statusBarLyricsEnable) {
val FLAG_ALWAYS_SHOW_TICKER = 0x01000000
val FLAG_ONLY_UPDATE_TICKER = 0x02000000
// Keep the status bar lyrics scrolling
flags = flags.or(FLAG_ALWAYS_SHOW_TICKER)
// Only update the ticker (lyrics), and do not update other properties
flags = flags.or(FLAG_ONLY_UPDATE_TICKER)
}
}

}

private fun buildBootstrapNotification(): Notification {
Expand Down Expand Up @@ -801,7 +830,7 @@ class AudioPlayerService : Service() {
updateNotification()
}

/** 鏋勫缓鎸囧悜鏈?Service 鐨?PendingIntent */
/** 构建指向本 Service PendingIntent */
private fun servicePendingIntent(action: String, requestCode: Int): PendingIntent {
return PendingIntent.getService(
this,
Expand Down Expand Up @@ -1010,6 +1039,11 @@ class AudioPlayerService : Service() {
updateNotification()
}
}

NPLogger.d(
"NERI-APS",
"cover bitmap=${bmp.width}x${bmp.height}, bytes=${bmp.byteCount / 1024 / 1024}MB"
)
} catch (e: Exception) {
NPLogger.d("NERI-APS", "Cover load failed: ${e.message}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ object PlayerManager {
internal var playbackCrossfadeOutDurationMs = DEFAULT_FADE_DURATION_MS
internal var playbackSoundConfig = PlaybackSoundConfig()
internal var lyriconEnabled = false
internal var statusBarLyricsEnable = false
internal var externalBluetoothLyricsEnabled = false
internal var cloudMusicLyricDefaultOffsetMs = DEFAULT_CLOUD_MUSIC_LYRIC_OFFSET_MS
internal var qqMusicLyricDefaultOffsetMs = DEFAULT_QQ_MUSIC_LYRIC_OFFSET_MS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ internal fun PlayerManager.initializeImpl(
}
}
}
ioScope.launch {
settingsRepo.statusBarLyricsEnabledFlow.collect { enabled ->
statusBarLyricsEnable = enabled
}
}
ioScope.launch {
settingsRepo.externalBluetoothLyricsEnabledFlow.collect { enabled ->
externalBluetoothLyricsEnabled = enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,8 +655,21 @@ object AutoSettingsSchema {
ui = SettingUiType.Switch
)
val lyriconEnabled = autoSetting(
titleRes = R.string.settings_lyricon_enabled,
descriptionRes = R.string.settings_lyricon_enabled_desc,
titleRes = R.string.settings_lyric_api_enabled,
descriptionRes = R.string.settings_lyric_api_enabled_desc,
iconRes = R.drawable.ic_lyricon
)

@AutoSetting(
key = "status_bar_lyrics_enabled",
type = SettingValueType.Boolean,
defaultBoolean = false,
order = 13,
ui = SettingUiType.Switch
)
val statusBarLyrics = autoSetting(
titleRes = R.string.settings_status_bar_lyrics_title,
descriptionRes = R.string.settings_status_bar_lyrics_summary,
iconRes = R.drawable.ic_lyricon
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ class SettingsRepository(private val context: Context) {
val lyriconEnabledFlow: Flow<Boolean> =
autoSettingsRepository.lyriconEnabledFlow

val statusBarLyricsEnabledFlow : Flow<Boolean> =
autoSettingsRepository.statusBarLyricsFlow

val externalBluetoothLyricsEnabledFlow: Flow<Boolean> =
autoSettingsRepository.externalBluetoothLyricsEnabledFlow

Expand Down
6 changes: 4 additions & 2 deletions app/src/main/res/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -967,8 +967,8 @@
<string name="settings_login_platforms_collapse">Collapse</string>
<string name="settings_motion">Motion Settings</string>
<string name="settings_motion_expand">Customize motion effects</string>
<string name="settings_lyricon_enabled">Lyricon Integration</string>
<string name="settings_lyricon_enabled_desc">Provide Lyricon with the current song, playback state, position, word-level lyrics, and translations</string>
<string name="settings_lyric_api_enabled">Lyricon Integration</string>
<string name="settings_lyric_api_enabled_desc">Provide Lyricon with the current song, playback state, position, word-level lyrics, and translations</string>
<string name="settings_external_bluetooth_lyrics_enabled">External Bluetooth Lyrics</string>
<string name="settings_external_bluetooth_lyrics_enabled_desc">When a Bluetooth device is connected, sync the current lyric line to the device song info area</string>
<string name="settings_lyrics_offset">Lyrics Settings</string>
Expand All @@ -978,6 +978,8 @@
<string name="settings_lyrics_offset_qq_music">QQ Music default offset</string>
<string name="settings_lyrics_offset_qq_music_desc">Default offset applied when loading QQ Music lyrics</string>
<string name="settings_lyrics_offset_value">Current offset: %1$s ms</string>
<string name="settings_status_bar_lyrics_title">Status Bar Lyrics</string>
<string name="settings_status_bar_lyrics_summary">Enable Meizu status bar lyrics (for select devices only)</string>

<!-- Now Playing / Empty State -->
<string name="nowplaying_no_playback">No Playback</string>
Expand Down
7 changes: 4 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -967,8 +967,8 @@
<string name="settings_login_platforms_collapse">收起</string>
<string name="settings_motion">动效设置</string>
<string name="settings_motion_expand">展开以调整动效表现</string>
<string name="settings_lyricon_enabled">词幕适配</string>
<string name="settings_lyricon_enabled_desc">向 Lyricon 提供当前歌曲、播放状态、进度、逐字歌词和翻译</string>
<string name="settings_lyric_api_enabled">第三方歌词API适配</string>
<string name="settings_lyric_api_enabled_desc">向 Lyricon &amp; SuperLyric 提供当前歌曲、播放状态、进度、逐字歌词和翻译</string>
<string name="settings_external_bluetooth_lyrics_enabled">外接设备蓝牙歌词</string>
<string name="settings_external_bluetooth_lyrics_enabled_desc">连接蓝牙设备时,将当前歌词行同步到外接设备的歌曲信息区域</string>
<string name="settings_lyrics_offset">歌词设置</string>
Expand All @@ -978,7 +978,8 @@
<string name="settings_lyrics_offset_qq_music">QQ 音乐默认歌词偏移</string>
<string name="settings_lyrics_offset_qq_music_desc">设置加载 QQ 音乐歌词时的基础偏移</string>
<string name="settings_lyrics_offset_value">当前偏移:%1$s ms</string>

<string name="settings_status_bar_lyrics_title">状态栏歌词</string>
<string name="settings_status_bar_lyrics_summary">启用魅族状态栏歌词(仅适用于部分设备)</string>
<!-- Now Playing / Empty State -->
<string name="nowplaying_no_playback">暂无播放</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ class AutoSettingsGeneratedTest {
R.drawable.ic_lyricon,
AutoSettingsSchema.lyrics.lyriconEnabled.iconRes
)
assertEquals(
R.drawable.ic_lyricon,
AutoSettingsSchema.lyrics.statusBarLyrics.iconRes
)
assertEquals(
AutoSettingIcon.BluetoothAudio,
AutoSettingsSchema.lyrics.externalBluetoothLyricsEnabled.icon
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ paletteKtx = "1.0.0"
reorderable = "0.9.6"
foundationLayout = "1.9.0"
securityCrypto = "1.1.0"
superlyricapi = "3.4"
taglib = "1.0.5"
uiGraphics = "1.9.0"
workRuntimeKtx = "2.11.0"
Expand All @@ -60,6 +61,7 @@ androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", versi
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "media3ExoplayerHls" }
androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" }
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" }
superlyricapi = { module = "com.github.HChenX:SuperLyricApi", version.ref = "superlyricapi" }
taglib = { module = "io.github.kyant0:taglib", version.ref = "taglib" }
androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version.ref = "uiGraphics" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
Expand Down
Loading