跳转至

apps\player\src\main\java\cn\nfarms\player\data\DemoAudio.kt

源码路径:apps\player\src\main\java\cn\nfarms\player\data\DemoAudio.kt · 106 行

package cn.nfarms.player.data

import java.io.File
import java.io.RandomAccessFile

/**
 * 演示音频生成器。
 *
 * 为什么要它:播放器**没有素材**就没法验证「断点续播」「变速」「歌词」这些功能,
 * 而把几个 MB 的 mp3 塞进仓库是不合适的。所以这里用纯代码合成一段
 * 16 位 PCM 的 WAV:体积小、可听、时长确定(便于断言进度)。
 *
 * 只造**单声道 22.05 kHz** —— 够听出音高差异,又不会让文件变大。
 */
object DemoAudio {

    /**
     * 造一段旋律,写到 [dir] 下,返回文件。
     *
     * @param seconds 时长(秒)
     * @param seed    换一个 seed 就是另一段旋律(用来生成"多首歌")
     */
    fun makeWav(dir: File, name: String, seconds: Int = 45, seed: Int = 1): File {
        dir.mkdirs()
        val file = File(dir, "$name.wav")
        if (file.isFile && file.length() > 1000) return file

        val sampleRate = 22_050
        val total = sampleRate * seconds
        val rnd = java.util.Random(seed.toLong())
        // 五声音阶,随便挑几个音高;每 0.4 秒换一个音
        val scale = intArrayOf(0, 2, 4, 7, 9, 12, 9, 7, 4, 2)
        val root = 220.0 * (1 + seed % 3)

        val pcm = ByteArray(total * 2)
        val segLen = (sampleRate * 0.4).toInt()
        var phase = 0.0
        for (i in 0 until total) {
            val seg = i / segLen
            val semi = scale[(seg + seed) % scale.size]
            val freq = root * Math.pow(2.0, semi / 12.0)
            phase += 2.0 * Math.PI * freq / sampleRate
            // 包络:每段起头淡入、结尾淡出,免得"咔"一声
            val inSeg = i % segLen
            val env = when {
                inSeg < 400 -> inSeg / 400.0
                inSeg > segLen - 400 -> (segLen - inSeg) / 400.0
                else -> 1.0
            }
            // 基频 + 一点点二次谐波,听起来不那么"电子"
            val v = (Math.sin(phase) * 0.6 + Math.sin(phase * 2) * 0.15) * env * 0.5
            val s = (v * Short.MAX_VALUE).toInt().coerceIn(-32768, 32767)
            pcm[i * 2] = (s and 0xFF).toByte()
            pcm[i * 2 + 1] = ((s shr 8) and 0xFF).toByte()
        }

        RandomAccessFile(file, "rw").use { raf ->
            raf.writeBytes("RIFF")
            raf.writeIntLE(36 + pcm.size)
            raf.writeBytes("WAVE")
            raf.writeBytes("fmt ")
            raf.writeIntLE(16)            // fmt chunk size
            raf.writeShortLE(1)           // PCM
            raf.writeShortLE(1)           // mono
            raf.writeIntLE(sampleRate)
            raf.writeIntLE(sampleRate * 2) // byte rate
            raf.writeShortLE(2)           // block align
            raf.writeShortLE(16)          // bits
            raf.writeBytes("data")
            raf.writeIntLE(pcm.size)
            raf.write(pcm)
        }
        rnd.nextInt() // 保持 seed 语义(内容已确定,这里只是占位)
        return file
    }

    /** 顺便造一份 LRC,让歌词页有东西看。 */
    fun makeLrc(dir: File, name: String, seconds: Int = 45): File {
        val f = File(dir, "$name.lrc")
        if (f.isFile) return f
        val sb = StringBuilder()
        sb.appendLine("[ti:$name]")
        sb.appendLine("[ar:NFarms Demo]")
        // 每 4 秒一句,一直铺到结尾
        var t = 0
        var i = 0
        while (t < seconds) {
            val mm = (t / 60).toString().padStart(2, '0')
            val ss = (t % 60).toString().padStart(2, '0')
            sb.appendLine("[$mm:$ss.00] 第 ${i + 1} 句歌词 —— 用来验证歌词跟随与跳转")
            t += 4
            i++
        }
        f.writeText(sb.toString())
        return f
    }

    private fun RandomAccessFile.writeIntLE(v: Int) {
        write(v and 0xFF); write((v shr 8) and 0xFF); write((v shr 16) and 0xFF); write((v shr 24) and 0xFF)
    }

    private fun RandomAccessFile.writeShortLE(v: Int) {
        write(v and 0xFF); write((v shr 8) and 0xFF)
    }
}