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)
}
}