完整案例:Kotlin+Compose+Multiplatform之桌面端音乐播放器,数据库使用实现(三)

循序渐进Kotlin+Compose+Multiplatform跨平台之桌面端实现系列:

完整案例:Kotlin+Compose+Multiplatform跨平台之桌面端实现(一)
完整案例:Kotlin+Compose+Multiplatform跨平台之桌面端实现(二)

效果图如下

一、前言

在前面两篇文章已经介绍了项目工程,打包配置,基础相关使用,

今天来到Kotlin+Compose+Multiplatform之桌面端

  • 数据库使用,
  • 音乐播放器,实现
  • 音频动画效果
  • LRC歌词展示

下面进行详细分解

二、数据库使用

Kotlin+Compose+Multiplatform之桌面端上面有很多数据库可以使用, 本案例采用介绍的是:

scss 复制代码
// Kotlinx.Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// SQLite JDBC 驱动
implementation("org.xerial:sqlite-jdbc:3.42.0.0")
  • 此方案
  1. 可以通过Kotlin序列化可直接将数据库查询结果映射为数据类
  2. 可以在编译器插件生成序列化代码,避免运行时反射开销
  3. 它支持跨平台数据格式转换(如SQLite结果→JSON→UI模型)
  4. SQLite+ JDBC的组合实现轻量级持久化,嵌入式数据库无需额外服务部署,特别适合桌面端
  5. 通过JDBC标准接口实现ACID事务,兼容所有Kotlin/JVM平台(Mac,Window,linux)
  6. 它与Compose状态管理无缝集成,支持协程异步操作
  • 接入配置:
  1. build.gradle中添加如下配置:
arduino 复制代码
plugins {
   kotlin("plugin.serialization") version "1.9.0" // Kotlin 序列化插件
}
  1. jvmMain.dependencies {}加依赖引入模块:
scss 复制代码
jvmMain.dependencies {
    // Kotlinx.Serialization
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
    // SQLite JDBC 驱动
    implementation("org.xerial:sqlite-jdbc:3.42.0.0")
}
  • 具体使用:
  1. 创建数据库获取到连接connection
kotlin 复制代码
private val dbName = "music.db"
private val connection: Connection by lazy {
    val dbPath = DatabaseUtils.getDatabasePath(dbName)
    DriverManager.getConnection("jdbc:sqlite:$dbPath")
}
  1. 创建表结构:通 connection.createStatement().executeUpdate直接可以执行创建表的sql语句
python 复制代码
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))
        )
    """
    )
}
  1. 插入一条数据如下:(注意:的顺序是从1开始依次往后连续加上去的
vbscript 复制代码
//插入播放列表
fun inserPlayItem(user: MusicItem): Int {
    val sql = "INSERT INTO play_list (musicID, name, singer, pic, url, lrc, musicSuffer,localFile) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    val statement = connection.prepareStatement(sql)
    statement.setString(1, user.musicID)
    statement.setString(2, user.name)
    statement.setString(3, user.singer)
    statement.setString(4, user.pic)
    statement.setString(5, user.url)
    statement.setString(6, user.lrc)
    statement.setString(7, user.musicSuffer)
    statement.setBoolean(8, user.localFile)
    statement.executeUpdate()
    return statement.generatedKeys.getInt(1) // 返回生成的 ID
}
  1. 从表中查询数据转化成实体列表:
vbscript 复制代码
//查询播放列表
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
}
  1. 操作数据库相关删除:
kotlin 复制代码
//删除播放列表,删除表结构
fun dropTable(): Int {
    val statement = connection.createStatement()
    // 执行删除表操作
    statement.executeUpdate("DROP TABLE IF EXISTS play_list")
    statement.close()
    return 1
}


//删除整个播放列表中数据
fun delList(): Int {
    val statement = connection.createStatement()
    // 执行清空操作
    statement.executeUpdate("DELETE FROM play_list")
    statement.close()
    return 1
}

//通过musicID 删除指定数据
fun delItemByID(musicID: String): Int {
    val statement = connection.prepareStatement(
        "DELETE FROM play_list WHERE musicID = ?"
    ).apply {
        setString(1, musicID)
    }
    val deletedRows = statement.executeUpdate()
    statement.close()
    return 1
}
  1. 对数据库中某条数据进行修改如下:
kotlin 复制代码
// 更新指定的字段值
fun updateField(musicID: String): Int {
    val sql = "UPDATE play_list SET localFile = ? WHERE musicID = ?"
    val statement = connection.prepareStatement(sql).apply {
        setInt(1, 1)
        setString(2, musicID)
        executeUpdate()
    }
    statement.close()
    return 1
}

三、音乐播放器

Kotlin+Compose+Multiplatform之桌面端上面有比较流行的方案:ComposeMultiplatformMediaPlayer方案,它是采用 采用跨平台媒体播放库,支持Windows/macOS/Linux三端统一开发,核心集成VLC引擎实现硬件加速解码,但是它要电脑安装好VLC环境配置,比如:Windows要安装 - VLC官方播放器

所以本案例不采用它,我们采用它

而是采用,电脑无需额外安装任何配置的,JavaFX提供开箱即用的Media/MediaPlaye即:org.openjfx:javafx

  • 它具备以下优势:
  1. 原生支持MP3/WAV/AAC等主流音频格式的解码播放,无需额外依赖第三方解码库
  2. 通过DirectX(Windows)/CoreAudio(macOS)等原生图形接口实现硬件级音频加速,显著降低CPU占用率
  3. 支持一套代码无缝运行于Windows/macOS/Linux,自动适配各平台音频驱动(如Windows WASAPI、Linux ALSA)开发者无需处理平台差异逻辑
  4. 它从JDK11起采用模块化设计,通过javafx-media模块精准控制依赖,应用打包体积比传统方案减少40%+
  • 接入相关配置:
  1. build.gradle中添加如下配置:
ini 复制代码
plugins {
    id("org.openjfx.javafxplugin") version "0.1.0"
}


javafx {
    version = "20.0.2"
    modules = listOf("javafx.media", "javafx.graphics", "javafx-swing")
}
  1. jvmMain.dependencies {}内添加实现播放器的相关依赖,如下:
javascript 复制代码
jvmMain.dependencies {
    val javafxPlatform = System.getProperty("os.name").lowercase().let {
        when {
            it.contains("linux") -> "linux"
            it.contains("mac") -> "mac"
            else -> "win"
        }
    }

    implementation("org.openjfx:javafx-base:20.0.2:$javafxPlatform") {
        exclude(group = "org.openjfx", module = "javafx-base")
    }  // 必须的基础模块:ml-citation{ref="7" data="citationList"}
    implementation("org.openjfx:javafx-media:20.0.2:$javafxPlatform") {
        exclude(group = "org.openjfx", module = "javafx-media")
    }
    implementation("org.openjfx:javafx-graphics:20.0.2:$javafxPlatform") {
        exclude(group = "org.openjfx", module = "javafx-graphics")
    }
    implementation("org.openjfx:javafx-swing:20.0.2:$javafxPlatform") {
        exclude(group = "org.openjfx", module = "javafx-swing")
    }
}
  • 使用步骤和相关API:
  1. 提前初始化 Platform.startup { }
  2. 播放加载音乐如下示代码:需要注意的是必须在主线程调用该方法:即,下面的代码必须包在 Platform.runLater{ }内部或者 SwingUtilities.invokeLater { } 的内部,否则会抛出IllegalStateException
scss 复制代码
 val media = Media(cacheFile.toURI().toString())
 mediaPlayer = MediaPlayer(media).apply {
                  setOnReady {
                       play()
                  }
              }
  1. 音乐 播放暂停,停止,是否正在播放 如下操作:
kotlin 复制代码
//提供是否正在播放
fun isPlaying() = mediaPlayer?.status == MediaPlayer.Status.PLAYING
//提供暂停播放方法
fun pause() = mediaPlayer?.pause()
//提供停止播放方法
fun stop() = mediaPlayer?.stop()
  1. 播放 前一首,下一首 :主要是通过 播放列表的当前索引切换:position + 1 后获得的播放资源重新播放即播放下一首,position - 1后获得的播放资源重新播放即播放上一首。 5. 获取播放的当前时间:val time = mediaPlayer?.currentTime?.toSeconds()?.toLong() ?: 0L 6. 获取播放总时长:` fun totalDuration(): Long = mediaPlayer?.totalDuration?.toSeconds()?.toLong() ?: 0L 7. 根据当前时间和总时长,可以计算出播放进度。 8. 拖动UI进度条seekTo,需要计算出拖动后的时间然后直接调用:mediaPlayer?.seek(Duration.seconds(seconds)) 9. 播放文件缓存配置:需要手动设置
less 复制代码
val cacheFile = if (!localFile) {
    File(PlatformKVStore.getDownloadDir(), downloadFileName).apply {
        DownLoadUtils.instance.WXDownload2(playItem.url, this@apply)
    }
} else {
    println("zou huan cun ")
    File(PlatformKVStore.getDownloadDir(), downloadFileName)
}

val media = Media(cacheFile.toURI().toString())

四、音频动画效果

本案例播放器音频动画效果:主要包含2个:

  1. 专辑封面转圈图片,
  2. 从播放器那边拿到的实时动态数据绘制条形变化,和转圈外围颜色条纹:
  • 从播放器里面拿到音频数据:
  1. 设置获取音频数据配置:在JavaFX媒体框架中,audioSpectrumIntervalaudioSpectrumNumBandsaudioSpectrumThreshold是用于控制音频频谱分析的核心参数,其中: audioSpectrumInterval 控制频谱数据更新的时间间隔(秒),默认值为0.1秒。较短的间隔可实现更实时的频谱反馈,但会增加CPU负载。audioSpectrumNumBands 指定频谱分析的频段数量,默认128个频段。增加频段数可提高频谱分辨率,但会提升计算复杂度。audioSpectrumThreshold 设置频谱灵敏度阈值(dB),默认-60dB。低于该阈值的音频信号将被过滤,避免显示噪声干扰。
    本案例配置如下:
ini 复制代码
 MediaPlayer(media).apply {
      audioSpectrumInterval = 0.03
      audioSpectrumNumBands = 320
      audioSpectrumThreshold = -60
      //监听频谱数据                  
      audioSpectrumListener = AudioSpectrumListener { _, _, magnitudes, phases ->
      
             //magnitudes 就是频谱数据,FloatArray类型
     
     }
  1. Compose UI 部分,通过.graphicsLayer { rotationZ} 修改rotationZ的值来选择封面图片
ini 复制代码
AsyncImage(
    model = picUrl, contentDescription = "唱片 Network Image", modifier = Modifier.padding(20.dp, 0.dp, 0.dp, 80.dp).size(300.dp).clip(CircleShape).border(
        width = 45.dp, color = Color.Black, shape = CircleShape
    ).padding(45.dp).graphicsLayer {
        rotationZ = viewModel.sheetDiskRotate.value
    }, contentScale = ContentScale.Crop
)
  1. 通过ComposeCanvas 来绘频谱数据,这个没什么难度。如下:条形频谱和圆圈周围动态频谱
scss 复制代码
//条形动态频谱
@Composable
fun AudioVisualizer(viewModel: PlayerViewModel) {
    var canvasWidth by remember { mutableFloatStateOf(0f) }
    val spectrumData by viewModel.spectrumDataFlow.collectAsState()
    Canvas(modifier = Modifier.padding(10.dp, 430.dp, 0.dp, 0.dp).width(300.dp).onSizeChanged { canvasWidth = it.width.toFloat() }) {
        if (spectrumData.isNotEmpty()) {
            drawRect(Color.Transparent)
            val barWidth = canvasWidth / spectrumData.size
            spectrumData.forEachIndexed { i, value ->

//                val newValue = value //if (value == -60.0f) randomInRange(-60f, -45f) else value
                val newValue = if (value == -60.0f) randomInRange(-60f, -45f) else value
                val height = (newValue + 60) * 3f  // 标准化幅度值
                val newIndex = i //if (i > haftSize) (i + haftSize) % dataSize else i + haftSize
                drawRect(
                    color = Color.hsv(newIndex * 360f / spectrumData.size, 1f, 1f), topLeft = Offset(newIndex * barWidth, size.height - height), size = Size(barWidth * 0.8f, height)
                )
            }
        }
    }
}

//圆圈周围动态频谱
@Composable
fun CircularAudioVisualizer(viewModel: PlayerViewModel) {
    var center by remember { mutableStateOf(Offset.Zero) }
    var baseRadius by remember { mutableStateOf(0f) }
    val spectrumData by viewModel.spectrumDataFlow.collectAsState()
    Canvas(modifier = Modifier.padding(20.dp, 0.dp, 0.dp, 80.dp).size(280.dp).onSizeChanged {
        center = Offset(it.width.toFloat() / 2, it.height.toFloat() / 2)
        baseRadius = min(it.width, it.height) * 0.55f
    }) {
        if (spectrumData.isNotEmpty()) {
            drawCircle(Color.Transparent, baseRadius, center, style = Stroke(2f))
            val angleStep = 2f * PI / spectrumData.size
            spectrumData.forEachIndexed { i, value ->
                val newValue = value //if (value == -60.0f) randomInRange(-60f, -46f) else value
                val normalized = newValue * 2 / 60f

                val spikeLength = baseRadius * 0.08f * normalized
                val angle = i * angleStep

                val startX = center.x + baseRadius * cos(angle).toFloat()
                val startY = center.y + baseRadius * sin(angle).toFloat()


                val endX = startX + spikeLength * cos(angle).toFloat()
                val endY = startY + spikeLength * sin(angle).toFloat()

                drawLine(
                    color = Color.hsv(i * 360f / spectrumData.size, 1f, 1f), start = Offset(startX, startY), end = Offset(endX, endY), strokeWidth = 6f
                )

                drawCircle(
                    color = Color.hsv(i * 360f / spectrumData.size, 1f, 1f), radius = 4f, center = Offset(endX, endY)
                )
            }
        }
    }
}

五、LRC歌词展示

LRC歌词展示中:这个没什么难度,使用Compose 的 LazyColumn展示歌词的每一行,获取到播放器当前播放时间,与解析LRC歌词头部时间进行对比,返回 LazyColumn中列表的位置索引,然后根据索引对当前歌词行文字 颜色,大小进行设置,详细请看项目地址源码:

六、总结

本篇是完整案例:Kotlin + Compose + Multiplatform之桌面端实现的第三篇,

主要介绍了:

  1. 数据库使用,
  2. 音乐播放器,实现
  3. 音频动画效果
  4. LRC歌词展示

相信会给你带来一定帮助。

后续还有:

  1. 视频播放介绍
  2. 桌面端插件化
  3. 跨端到 移动端,桌面端,web端的公用研究。

源码地址

感谢阅读:

欢迎用你发财的小手 关注,点赞、收藏

这里你会学到不一样的东西

相关推荐
LoserChaser2 小时前
Android—服务+通知=>前台服务
android
2501_916008893 小时前
iOS混淆工具有哪些?在集成第三方 SDK 时的混淆策略与工具建议
android·ios·小程序·https·uni-app·iphone·webview
2501_915921433 小时前
Windows 如何上架 iOS 应用?签名上传全流程 + 工具推荐
android·ios·小程序·https·uni-app·iphone·webview
曾经的三心草4 小时前
微服务的编程测评系统10-竞赛删除发布-用户管理-登录注册
微服务·云原生·架构
爷_4 小时前
用 Python 打造你的专属 IOC 容器
后端·python·架构
极客奇点6 小时前
存储成本深度优化:冷热分层与生命周期管理——从视频平台年省200万实践解析智能存储架构
阿里云·架构·降本增效·云存储·云成本优化
helloworld工程师6 小时前
Dubbo应用开发之架构的演进之路
架构·dubbo
启山智软6 小时前
什么是单体架构?什么是微服务架构?
微服务·架构
小小琪_Bmob后端云6 小时前
【Trae实践】直播间自动发言工具的开发
android·ai编程·trae