test.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. PROJECT = "pcm32_spectrum"
  2. VERSION = "1.0.0"
  3. _G.sys = require("sys")
  4. -- 32bit PCM频谱分析
  5. function generate_32bit_spectrum(pcm_filename, output_filename)
  6. local sample_rate = 24000
  7. local fft_points = 256 -- 使用较小的FFT点数节省内存
  8. local df = sample_rate / fft_points
  9. log.info("分析", "32bit PCM频谱分析")
  10. log.info("参数", string.format("采样率: %dHz, FFT点数: %d", sample_rate, fft_points))
  11. -- 检查文件是否存在
  12. if not io.exists(pcm_filename) then
  13. log.error("文件", "PCM文件不存在: " .. pcm_filename)
  14. return false
  15. end
  16. -- 获取文件大小
  17. local file_size = io.fileSize(pcm_filename)
  18. if not file_size then
  19. log.error("文件", "无法获取文件大小")
  20. return false
  21. end
  22. log.info("文件", string.format("文件大小: %d 字节", file_size))
  23. log.info("文件", string.format("约 %d 个32bit样本", file_size // 4))
  24. -- 准备FFT缓冲区
  25. local real_buf = zbuff.create(fft_points * 2)
  26. local imag_buf = zbuff.create(fft_points * 2)
  27. local Wc = zbuff.create((fft_points // 2) * 2)
  28. local Ws = zbuff.create((fft_points // 2) * 2)
  29. fft.generate_twiddles_q15_to_zbuff(fft_points, Wc, Ws)
  30. -- 读取文件
  31. local file = io.open(pcm_filename, "rb")
  32. if not file then
  33. log.error("文件", "无法打开PCM文件")
  34. return false
  35. end
  36. -- 读取一个片段(32bit = 4字节 per sample)
  37. local bytes_to_read = fft_points * 4
  38. local chunk = file:read(bytes_to_read)
  39. file:close()
  40. if not chunk or #chunk < 4 then
  41. log.error("读取", "文件数据不足")
  42. return false
  43. end
  44. log.info("读取", string.format("读取了 %d 字节", #chunk))
  45. -- 处理32bit PCM数据
  46. local samples_processed = 0
  47. for i = 1, math.floor(#chunk / 4) do
  48. local byte_pos = (i - 1) * 4 + 1
  49. -- 32bit小端序解析
  50. local b1 = chunk:byte(byte_pos) -- 最低字节
  51. local b2 = chunk:byte(byte_pos + 1)
  52. local b3 = chunk:byte(byte_pos + 2)
  53. local b4 = chunk:byte(byte_pos + 3) -- 最高字节
  54. -- 组合成32位整数
  55. local sample = b1 + b2 * 256 + b3 * 65536 + b4 * 16777216
  56. -- 32bit有符号转换 (范围: -2147483648 到 2147483647)
  57. if sample >= 2147483648 then
  58. sample = sample - 4294967296
  59. end
  60. -- 32bit转U12格式
  61. local normalized = (sample / 2147483648.0) -- 归一化到 -1.0 到 1.0
  62. local u12_val = math.floor((normalized + 1.0) * 2047.5 + 0.5) -- 转到 0-4095
  63. if u12_val < 0 then u12_val = 0 end
  64. if u12_val > 4095 then u12_val = 4095 end
  65. real_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  66. real_buf:writeU16(u12_val)
  67. imag_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  68. imag_buf:writeU16(0)
  69. samples_processed = samples_processed + 1
  70. -- 每处理64个样本输出一次进度
  71. if i % 64 == 0 then
  72. log.info("处理", string.format("已处理 %d/%d 样本", i, math.floor(#chunk / 4)))
  73. end
  74. end
  75. -- 如果数据不足,用0填充
  76. for i = samples_processed + 1, fft_points do
  77. real_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  78. real_buf:writeU16(2048) -- 中间值
  79. imag_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  80. imag_buf:writeU16(0)
  81. end
  82. log.info("FFT", "开始执行FFT计算...")
  83. -- 执行FFT
  84. fft.run(real_buf, imag_buf, fft_points, Wc, Ws, {
  85. core = "q15",
  86. input_format = "u12"
  87. })
  88. -- 生成输出文件
  89. local out_file = io.open(output_filename, "w")
  90. if not out_file then
  91. log.error("文件", "无法创建输出文件")
  92. return false
  93. end
  94. -- 写入文件头
  95. local ret, err = out_file:write('{\n')
  96. if not ret then
  97. log.error("写入", "写入文件头失败:", err)
  98. out_file:close()
  99. return false
  100. end
  101. out_file:write(' "sample_rate": 24000,\n')
  102. out_file:write(' "fft_points": 256,\n')
  103. out_file:write(' "frequency_resolution": ' .. df .. ',\n')
  104. out_file:write(' "max_frequency": 12000,\n')
  105. out_file:write(' "bit_depth": 32,\n')
  106. out_file:write(' "spectrum_data": [\n')
  107. -- 收集频谱数据
  108. local freq_data = {}
  109. local max_magnitude = 0
  110. -- 先扫描找到最大值用于归一化
  111. for k = 1, fft_points // 2 do
  112. real_buf:seek(k * 2, zbuff.SEEK_SET)
  113. imag_buf:seek(k * 2, zbuff.SEEK_SET)
  114. local r = real_buf:readI16()
  115. local i = imag_buf:readI16()
  116. local magnitude = math.sqrt(r * r + i * i)
  117. if magnitude > max_magnitude then
  118. max_magnitude = magnitude
  119. end
  120. end
  121. -- 生成归一化的频谱数据
  122. for k = 1, fft_points // 2 do
  123. real_buf:seek(k * 2, zbuff.SEEK_SET)
  124. imag_buf:seek(k * 2, zbuff.SEEK_SET)
  125. local r = real_buf:readI16()
  126. local i = imag_buf:readI16()
  127. local magnitude = math.sqrt(r * r + i * i)
  128. -- 归一化到0-100范围
  129. local normalized = 0
  130. if max_magnitude > 0 then
  131. normalized = (magnitude / max_magnitude) * 100
  132. end
  133. table.insert(freq_data, string.format("%.2f", normalized))
  134. -- 输出主要频点信息
  135. if normalized > 10 then -- 只显示能量大于10%的频点
  136. local freq = (k - 1) * df
  137. log.info("频点", string.format("%.0fHz: %.1f%%", freq, normalized))
  138. end
  139. end
  140. -- 写入频谱数据
  141. out_file:write(' { "time": 0.0, "freq": [')
  142. out_file:write(table.concat(freq_data, ", "))
  143. out_file:write('] }\n')
  144. out_file:write(' ]\n')
  145. out_file:write('}\n')
  146. out_file:close()
  147. log.info("完成", "32bit PCM频谱分析完成")
  148. log.info("文件", "频谱数据已保存到: " .. output_filename)
  149. return true
  150. end
  151. -- 快速测试版本(不生成文件,直接输出)
  152. function quick_32bit_test(pcm_filename)
  153. local sample_rate = 24000
  154. local fft_points = 128
  155. log.info("快速测试", "32bit PCM快速频谱分析")
  156. -- 检查文件是否存在
  157. if not io.exists(pcm_filename) then
  158. log.error("文件", "PCM文件不存在")
  159. return false
  160. end
  161. -- 最小内存分配
  162. local real_buf = zbuff.create(fft_points * 2)
  163. local imag_buf = zbuff.create(fft_points * 2)
  164. local Wc = zbuff.create((fft_points // 2) * 2)
  165. local Ws = zbuff.create((fft_points // 2) * 2)
  166. fft.generate_twiddles_q15_to_zbuff(fft_points, Wc, Ws)
  167. local file = io.open(pcm_filename, "rb")
  168. if not file then
  169. log.error("文件", "无法打开文件")
  170. return false
  171. end
  172. local chunk = file:read(512) -- 只读512字节
  173. file:close()
  174. if not chunk then
  175. log.error("读取", "读取文件失败")
  176. return false
  177. end
  178. log.info("读取", string.format("读取了 %d 字节", #chunk))
  179. -- 处理32bit数据
  180. local samples_processed = 0
  181. for i = 1, math.min(fft_points, math.floor(#chunk / 4)) do
  182. local byte_pos = (i - 1) * 4 + 1
  183. if byte_pos + 3 <= #chunk then
  184. local b1 = chunk:byte(byte_pos)
  185. local b2 = chunk:byte(byte_pos + 1)
  186. local b3 = chunk:byte(byte_pos + 2)
  187. local b4 = chunk:byte(byte_pos + 3)
  188. local sample = b1 + b2 * 256 + b3 * 65536 + b4 * 16777216
  189. if sample >= 2147483648 then sample = sample - 4294967296 end
  190. -- 简化转换
  191. local normalized = sample / 2147483648.0
  192. local u12_val = math.floor((normalized + 1.0) * 2047.5)
  193. if u12_val < 0 then u12_val = 0 end
  194. if u12_val > 4095 then u12_val = 4095 end
  195. real_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  196. real_buf:writeU16(u12_val)
  197. imag_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  198. imag_buf:writeU16(0)
  199. samples_processed = samples_processed + 1
  200. end
  201. end
  202. -- 填充剩余数据
  203. for i = samples_processed + 1, fft_points do
  204. real_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  205. real_buf:writeU16(2048)
  206. imag_buf:seek((i - 1) * 2, zbuff.SEEK_SET)
  207. imag_buf:writeU16(0)
  208. end
  209. -- FFT计算
  210. fft.run(real_buf, imag_buf, fft_points, Wc, Ws, {
  211. core = "q15",
  212. input_format = "u12"
  213. })
  214. -- 直接输出结果
  215. log.info("=== 频谱结果 ===")
  216. local df = sample_rate / fft_points
  217. local peaks = {}
  218. for k = 2, fft_points // 2 do -- 跳过直流分量
  219. real_buf:seek(k * 2, zbuff.SEEK_SET)
  220. imag_buf:seek(k * 2, zbuff.SEEK_SET)
  221. local r = real_buf:readI16()
  222. local i = imag_buf:readI16()
  223. local magnitude = math.sqrt(r * r + i * i)
  224. local freq = (k - 1) * df
  225. if magnitude > 100 then
  226. table.insert(peaks, { freq = freq, mag = magnitude })
  227. end
  228. end
  229. -- 按幅度排序,显示前10个
  230. table.sort(peaks, function(a, b) return a.mag > b.mag end)
  231. for i = 1, math.min(10, #peaks) do
  232. log.info("峰值" .. i, string.format("%.0fHz (%.1f)", peaks[i].freq, peaks[i].mag))
  233. end
  234. if #peaks == 0 then
  235. log.info("结果", "未检测到明显频率峰值")
  236. end
  237. return true
  238. end
  239. -- 极简版本:只分析前几个样本
  240. function minimal_32bit_test(pcm_filename)
  241. log.info("极简测试", "32bit PCM极简分析")
  242. if not io.exists(pcm_filename) then
  243. log.error("文件", "文件不存在")
  244. return false
  245. end
  246. local file_size = io.fileSize(pcm_filename)
  247. if not file_size or file_size < 16 then
  248. log.error("文件", "文件太小")
  249. return false
  250. end
  251. log.info("文件", string.format("文件大小: %d 字节", file_size))
  252. -- 只读取前16个字节(4个样本)
  253. local data = io.readFile(pcm_filename, "rb", 0, 16)
  254. if not data then
  255. log.error("读取", "读取文件失败")
  256. return false
  257. end
  258. log.info("数据", string.format("读取了 %d 字节", #data))
  259. -- 分析前4个样本
  260. for i = 1, math.min(4, math.floor(#data / 4)) do
  261. local byte_pos = (i - 1) * 4 + 1
  262. if byte_pos + 3 <= #data then
  263. local b1 = data:byte(byte_pos)
  264. local b2 = data:byte(byte_pos + 1)
  265. local b3 = data:byte(byte_pos + 2)
  266. local b4 = data:byte(byte_pos + 3)
  267. local sample = b1 + b2 * 256 + b3 * 65536 + b4 * 16777216
  268. if sample >= 2147483648 then sample = sample - 4294967296 end
  269. local normalized = sample / 2147483648.0
  270. log.info("样本" .. i, string.format("原始值: %d, 归一化: %.6f", sample, normalized))
  271. end
  272. end
  273. return true
  274. end
  275. -- 主程序
  276. sys.taskInit(function()
  277. if not fft then
  278. log.error("FFT", "不支持FFT库")
  279. return
  280. end
  281. sys.wait(3000) -- 等待系统稳定
  282. local pcm_file = "/luadb/qlx_13sec.pcm"
  283. local spectrum_file = "/spectrum.json"
  284. if not io.exists(pcm_file) then
  285. log.warn("文件", "PCM文件不存在: " .. pcm_file)
  286. log.info("提示", "请将32bit PCM文件命名为 audio.pcm 并放入文件系统")
  287. return
  288. end
  289. log.info("开始", "32bit PCM频谱分析...")
  290. -- 先尝试极简测试
  291. if minimal_32bit_test(pcm_file) then
  292. log.info("成功", "极简测试完成")
  293. -- 然后尝试快速测试
  294. sys.wait(1000)
  295. if quick_32bit_test(pcm_file) then
  296. log.info("成功", "快速测试完成")
  297. -- 最后进行完整分析
  298. sys.wait(1000)
  299. if generate_32bit_spectrum(pcm_file, spectrum_file) then
  300. log.info("完成", "完整频谱分析完成!")
  301. -- 检查输出文件
  302. if io.exists(spectrum_file) then
  303. local out_size = io.fileSize(spectrum_file)
  304. if out_size then
  305. log.info("输出", string.format("频谱文件大小: %d 字节", out_size))
  306. end
  307. end
  308. else
  309. log.error("失败", "完整频谱分析失败")
  310. end
  311. else
  312. log.error("失败", "快速测试失败")
  313. end
  314. else
  315. log.error("失败", "极简测试失败")
  316. end
  317. log.info("程序", "分析流程结束")
  318. require("tcp_test")
  319. dtuDemo(1, "112.125.89.8", 32062)
  320. end)
  321. require("test")