找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1426

积分

0

好友

208

主题
发表于 16 小时前 | 查看: 2| 回复: 0

播放器效果图

本文是《Kotlin+Compose+Multiplatform跨平台桌面端实现》系列的第三篇,将聚焦于播放器项目的核心功能开发,涵盖本地数据持久化、音频播放与动画效果以及歌词展示等关键模块的实现。

一、数据库的选型与使用

在Kotlin Compose Multiplatform桌面项目中,有多种数据库方案可选。本案例采用 SQLiteJDBC 驱动的组合,配合 Kotlinx.Serialization 实现高效的数据存取。这套方案的优势在于:

  1. 轻量级与跨平台:SQLite是嵌入式数据库,无需单独部署服务,非常适合桌面应用。JDBC标准接口确保了在Windows、macOS和Linux上的兼容性。
  2. 高性能序列化:通过Kotlinx.Serialization编译器插件,可将查询结果直接映射为数据类,避免了运行时的反射开销,提升了性能。
  3. 无缝集成:该方案能与Compose的响应式状态管理及协程异步操作良好结合。

接入配置

首先,在 build.gradle.kts 中启用序列化插件,并在公共依赖中引入相关库。

// 1. 应用插件
plugins {
    kotlin("plugin.serialization") version "1.9.0"
}

// 2. 在 `commonMain` 或 `jvmMain` 的 dependencies 中添加
kotlin {
    sourceSets {
        val jvmMain by getting {
            dependencies {
                // Kotlin 序列化
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
                // SQLite JDBC 驱动
                implementation("org.xerial:sqlite-jdbc:3.42.0.0")
            }
        }
    }
}

核心操作实践

以下是数据库连接、建表、增删改查等核心操作的代码示例。

1. 建立数据库连接

private val dbName = "music.db"
private val connection: Connection by lazy {
    val dbPath = DatabaseUtils.getDatabasePath(dbName) // 自定义工具获取路径
    DriverManager.getConnection("jdbc:sqlite:$dbPath")
}

2. 创建数据表

private fun createTable() {
    connection.createStatement().executeUpdate(
        """
        CREATE TABLE IF NOT EXISTS play_list (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            musicID TEXT NOT NULL,
            name TEXT NOT NULL,
            singer TEXT NOT NULL,
            pic TEXT NOT NULL,
            url TEXT NOT NULL,
            lrc TEXT NOT NULL,
            musicSuffer TEXT NOT NULL,
            localFile INTEGER CHECK (localFile IN (0, 1))
        )
        """
    )
}

3. 插入数据

// 插入播放列表项,并返回自增ID
fun inserPlayItem(item: MusicItem): Int {
    val sql = "INSERT INTO play_list (musicID, name, singer, pic, url, lrc, musicSuffer, localFile) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    connection.prepareStatement(sql).apply {
        setString(1, item.musicID)
        setString(2, item.name)
        setString(3, item.singer)
        setString(4, item.pic)
        setString(5, item.url)
        setString(6, item.lrc)
        setString(7, item.musicSuffer)
        setBoolean(8, item.localFile)
        executeUpdate()
        return generatedKeys.getInt(1) // 返回生成的ID
    }
}

4. 查询数据

// 查询所有播放列表数据
fun getAllPlayList(): List<MusicItem> {
    val list = mutableListOf<MusicItem>()
    val resultSet = connection.createStatement().executeQuery("SELECT * FROM play_list ORDER BY id ASC")
    while (resultSet.next()) {
        list.add(
            MusicItem(
                resultSet.getString("musicID"),
                resultSet.getString("name"),
                resultSet.getString("singer"),
                resultSet.getString("pic"),
                resultSet.getString("url"),
                resultSet.getString("lrc"),
                resultSet.getString("musicSuffer"),
                resultSet.getBoolean("localFile"),
                resultSet.getInt("id")
            )
        )
    }
    return list
}

5. 删除与更新操作

// 根据ID删除单项
fun delItemByID(musicID: String): Int {
    connection.prepareStatement("DELETE FROM play_list WHERE musicID = ?").apply {
        setString(1, musicID)
        executeUpdate()
        close()
    }
    return 1
}

// 更新指定字段(例如标记为本地文件)
fun updateField(musicID: String): Int {
    connection.prepareStatement("UPDATE play_list SET localFile = ? WHERE musicID = ?").apply {
        setInt(1, 1)
        setString(2, musicID)
        executeUpdate()
        close()
    }
    return 1
}

通过标准的 JDBC 操作,我们实现了桌面应用中对 数据库 的完整管理。

二、跨平台音频播放器实现

对于桌面端的音频播放,本案例没有选择需要额外安装VLC环境的 ComposeMultiplatformMediaPlayer,而是采用了 JavaFX Media API (org.openjfx:javafx-media)。

优势分析:

  1. 开箱即用:内置主流音频格式解码(MP3/WAV/AAC等),无需用户安装额外软件。
  2. 硬件加速:利用各平台原生音频接口(如Windows WASAPI、macOS CoreAudio)实现高效播放,CPU占用低。
  3. 模块化精简:JDK11后采用模块化设计,可按需引入javafx-media,有效控制应用体积。

接入与配置

build.gradle.kts 中配置 JavaFX 插件和平台特定依赖。

plugins {
    id("org.openjfx.javafxplugin") version "0.1.0"
}

javafx {
    version = "20.0.2"
    modules = listOf("javafx.media", "javafx.graphics")
}

kotlin {
    sourceSets {
        val jvmMain by getting {
            dependencies {
                // 根据运行平台动态引入对应依赖
                val osName = System.getProperty("os.name").lowercase()
                val platform = when {
                    osName.contains("linux") -> "linux"
                    osName.contains("mac") -> "mac"
                    else -> "win"
                }
                implementation("org.openjfx:javafx-media:20.0.2:$platform")
                implementation("org.openjfx:javafx-graphics:20.0.2:$platform")
            }
        }
    }
}

播放器核心API使用

1. 初始化与播放

import javafx.application.Platform
import javafx.scene.media.*

// 初始化JavaFX环境
Platform.startup { }

// 加载并播放媒体(必须在UI线程,如SwingUtilities.invokeLater或Platform.runLater中)
val media = Media(audioFile.toURI().toString())
mediaPlayer = MediaPlayer(media).apply {
    setOnReady {
        play()
    }
}

2. 控制播放状态

// 播放/暂停/停止
fun play() = mediaPlayer?.play()
fun pause() = mediaPlayer?.pause()
fun stop() = mediaPlayer?.stop()
fun isPlaying() = mediaPlayer?.status == MediaPlayer.Status.PLAYING

// 上一首/下一首:通过管理播放列表索引,重新加载对应Media并播放实现
fun playNext() {
    currentIndex = (currentIndex + 1) % playList.size
    loadAndPlay(playList[currentIndex])
}

3. 播放进度控制

// 获取当前时间与总时长(秒)
val currentTime: Long = mediaPlayer?.currentTime?.toSeconds()?.toLong() ?: 0L
val totalDuration: Long = mediaPlayer?.totalDuration?.toSeconds()?.toLong() ?: 0L

// 跳转到指定位置
fun seekTo(seconds: Double) {
    mediaPlayer?.seek(Duration.seconds(seconds))
}

4. 音频文件缓存策略
为了提高加载速度和应对网络资源,可以实现一个简单的缓存机制。

fun playWithCache(item: MusicItem) {
    val cacheFile = if (!item.localFile) {
        // 非本地文件,下载到缓存目录
        val targetFile = File(PlatformKVStore.getDownloadDir(), "${item.musicID}.mp3")
        DownLoadUtils.instance.download(item.url, targetFile)
        targetFile
    } else {
        // 本地文件直接使用
        File(item.url)
    }
    val media = Media(cacheFile.toURI().toString())
    // ... 创建MediaPlayer并播放
}

这种基于 JavaFX 的播放方案,是构建跨平台 Android 或桌面多媒体应用的稳定选择之一。

三、音频可视化动画效果

为提升播放器视觉体验,我们实现两种动画效果:旋转的专辑封面和动态音频频谱。

1. 获取实时音频频谱数据
通过配置 MediaPlayer 的频谱监听器获取原始数据。

MediaPlayer(media).apply {
    // 配置频谱分析参数
    audioSpectrumInterval = 0.03 // 更新间隔(秒),越短越实时,CPU消耗越高
    audioSpectrumNumBands = 320  // 频段数量,越多分辨率越高
    audioSpectrumThreshold = -60 // 灵敏度阈值(dB),过滤环境噪音

    audioSpectrumListener = AudioSpectrumListener { _, _, magnitudes, _ ->
        // magnitudes: FloatArray,即实时的频谱幅度数据
        viewModel.updateSpectrumData(magnitudes)
    }
}

2. Compose 实现旋转专辑封面
在UI中,通过动态修改图形的旋转角度来实现。

@Composable
fun RotatingAlbumCover(picUrl: String, rotationAngle: Float) {
    AsyncImage(
        model = picUrl,
        contentDescription = "唱片封面",
        modifier = Modifier
            .size(300.dp)
            .clip(CircleShape)
            .border(2.dp, Color.Gray, CircleShape)
            .graphicsLayer {
                rotationZ = rotationAngle // 关键:动态绑定旋转角度
            },
        contentScale = ContentScale.Crop
    )
}
// ViewModel中通过协程或动画API持续更新rotationAngle的值

3. 绘制动态频谱图
使用 Compose 的 Canvas 绘制条形频谱和环形频谱。

// 条形频谱
@Composable
fun BarVisualizer(spectrumData: List<Float>) {
    Canvas(modifier = Modifier.fillMaxWidth().height(100.dp)) {
        if (spectrumData.isNotEmpty()) {
            val barWidth = size.width / spectrumData.size
            spectrumData.forEachIndexed { i, value ->
                val height = (value + 60).coerceAtLeast(0f) * 2f // 数据标准化
                drawRect(
                    color = Color.hsv(i * 360f / spectrumData.size, 1f, 1f),
                    topLeft = Offset(i * barWidth, size.height - height),
                    size = Size(barWidth * 0.8f, height)
                )
            }
        }
    }
}

通过上述组合,将音频的物理特性转化为直观的视觉反馈,增强了应用的交互表现力。

四、LRC歌词同步展示

歌词展示的核心逻辑是时间轴同步。我们需要解析LRC文件,并将其与播放器的当前时间进行比对。

1. 数据模型

data class LrcLine(
    val time: Long, // 时间戳(毫秒)
    val text: String // 歌词文本
)

2. 同步逻辑
ViewModel 中,根据当前播放时间计算应高亮的歌词行索引。

fun findCurrentLyricIndex(currentTimeMs: Long, lrcLines: List<LrcLine>): Int {
    // 找到最后一个时间戳小于等于当前时间的歌词行
    for (i in lrcLines.indices.reversed()) {
        if (lrcLines[i].time <= currentTimeMs) {
            return i
        }
    }
    return 0
}

3. Compose UI 实现
使用 LazyColumn 展示歌词列表,并根据当前索引调整样式。

@Composable
fun LyricDisplay(lrcLines: List<LrcLine>, currentIndex: Int) {
    LazyColumn(
        horizontalAlignment = Alignment.CenterHorizontally,
        state = rememberLazyListState(initialFirstVisibleItemIndex = currentIndex.coerceAtLeast(0))
    ) {
        itemsIndexed(lrcLines) { index, line ->
            Text(
                text = line.text,
                fontSize = if (index == currentIndex) 22.sp else 16.sp,
                color = if (index == currentIndex) Color.Blue else Color.Gray,
                modifier = Modifier.padding(8.dp)
            )
        }
    }
}

五、总结与展望

本文详细介绍了在 Kotlin Compose Multiplatform 桌面项目中实现音乐播放器核心模块的全过程,涵盖了 SQLite数据库操作JavaFX跨平台音频播放音频频谱可视化以及LRC歌词同步 四大关键技术点。

这一 架构 将数据持久化、多媒体处理与现代化UI框架紧密结合,为构建功能丰富的桌面应用提供了可行路径。本系列后续将探讨视频播放、桌面端插件化以及如何将代码共享至移动端与Web端等其他跨平台场景。

项目完整源码:
https://github.com/wgllss/Kotlin_Compose_Multiplatform_Demo

项目演示




上一篇:长文本建模实战指南:BERT、Longformer、BigBird与RoPE原理、选型与工程落地
下一篇:AI时代程序员的未来:初级开发者危机与高阶能力重塑
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 17:25 , Processed in 0.200429 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表