You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Common Lisp中通过cl-portaudio正确播放FM解调信号?

解决cl-portaudio播放FM解调信号的问题

咱们一步步来拆解你遇到的三个核心问题:信号削波、类型警告,以及ALSA underrun错误,逐个解决后就能正常播放FM信号了。

1. 修复信号幅度范围,消除削波声

你现在听到的削波声,本质是解调后的信号幅度超出了音频设备能处理的范围:

  • 首先,atan-complex-number的输出范围是[-π, π](大概-3.14到3.14),但音频播放要求信号归一化到[-1.0, 1.0]的单精度浮点数。所以要把解调结果除以π,把幅度压缩到合法区间:
    (vector-push (/ (atan-complex-number dif-complex) pi) samples-array)
    
  • 其次,你的IQ采样是sc16-q11格式:16位有符号整数,但只有低11位是有效数据,高5位是符号扩展。在转换为复数前,必须先把采样值归一化到[-1.0, 1.0],否则复数幅度会过大,导致解调信号波动更剧烈:
    ;; 把sc16-q11采样转换为标准化浮点数
    (let* ((previous-i (/ (mem-aref rx-samples :int16 (1+ i)) (expt 2 10))) ; q11的缩放因子是2^10
           (previous-q (/ (mem-aref rx-samples :int16 i) (expt 2 10)))
           (current-i (/ (mem-aref rx-samples :int16 (+ i 3)) (expt 2 10)))
           (current-q (/ (mem-aref rx-samples :int16 (+ i 2)) (expt 2 10)))
           ...)
    

2. 消除类型警告,确保数组类型严格匹配

cl-portaudio给出的类型警告,是因为你创建的数组虽然元素是float,但没有明确指定是单精度浮点(对应C的float类型,也就是cl-portaudio要求的:float格式)。修改数组初始化代码,明确类型:

(samples-array (make-array 4096 ; 你每次处理4096个IQ对,对应4096个解调样本
                           :element-type 'single-float
                           :initial-element 0.0s0)) ; 用0.0s0表示单精度浮点数

另外,你之前把数组大小设为8192是多余的,因为循环只生成4096个样本,调整后还能避免空间浪费。

3. 解决ALSA Underrun(缓冲区欠载)问题

这个错误说明音频设备的播放缓冲区空了——你的数据供给速度跟不上播放速度,核心问题有两个:

  • 每次调用fm-demodulate-sc16-q11-samples都重复初始化bladeRF设备(配置信道、启动同步、启用模块),这会带来巨大的延迟;
  • 解调后的信号采样率(和bladeRF的采样率一致,你这里设的是40MHz)远高于音频播放的44100Hz,直接播放会导致设备处理不过来。

修复方案:重构代码,复用设备初始化+重采样

第一步:拆分设备初始化和解调逻辑

把设备初始化单独抽出来,只执行一次,不要每次获取样本都重复操作:

(defun init-fm-receiver (device frequency)
  ;; 注意:这里的configure-channel参数需要确认你的绑定定义,通常是(信道 频率 带宽 采样率 增益)
  ;; 建议把采样率调低到合理值,比如4MHz,减少后续重采样压力
  (configure-channel device (channel-rx 0) frequency 1000000 4000000 30)
  (init-sync device)
  (enable-module device (channel-rx 0) t))

第二步:实现持续获取解调样本的函数

(defun get-demodulated-fm-samples (device num-iq-pairs)
  (with-foreign-objects ((rx-samples :uint16 (* num-iq-pairs 2)) ; 每个IQ样本占2个uint16
                         (metadata :pointer))
    (let ((status (bladerf_sync_rx (mem-ref device :pointer) rx-samples num-iq-pairs metadata 5000))
          (samples-array (make-array num-iq-pairs
                                     :element-type 'single-float
                                     :initial-element 0.0s0)))
      (if (< status 0)
          (error "Failed to receive IQ samples error: ~S" status)
          (progn
            (loop for i below (* num-iq-pairs 2) by 2
                  for idx from 0 do
                  (let* ((prev-i (/ (mem-aref rx-samples :int16 (+ i 1)) (expt 2 10)))
                         (prev-q (/ (mem-aref rx-samples :int16 i) (expt 2 10)))
                         (curr-i (/ (mem-aref rx-samples :int16 (+ i 3)) (expt 2 10)))
                         (curr-q (/ (mem-aref rx-samples :int16 (+ i 2)) (expt 2 10)))
                         (prev-complex (complex prev-i prev-q))
                         (curr-complex (complex curr-i curr-q))
                         (dif-complex (* (conjugate prev-complex) curr-complex)))
                    (setf (aref samples-array idx) (/ (atan-complex-number dif-complex) pi))))
            samples-array)))))

第三步:播放时重采样,匹配音频采样率

必须把高采样率的FM解调信号重采样到44100Hz才能正常播放。这里用简单的抽取示例,实际可以用cl-resample库获得更平滑的音质:

(defconstant +frames-per-buffer+ 1024)
(defconstant +sample-rate+ 44100d0)
(defconstant +sample-format+ :float)
(defconstant +num-channels+ 2)

(defun play-demodulated-fm (device)
  (let ((fm-sample-rate 4000000) ; 对应init-fm-receiver里设置的bladeRF采样率
        (audio-sample-rate +sample-rate+))
    (with-audio
      (with-default-audio-stream (astream +num-channels+ +num-channels+
                                          :sample-format +sample-format+
                                          :sample-rate audio-sample-rate
                                          :frames-per-buffer +frames-per-buffer+)
        ;; 只初始化一次FM接收器
        (init-fm-receiver device 863000000)
        (unwind-protect
             (loop
               ;; 获取足够的FM样本,计算需要的IQ对数量
               (let* ((required-fm-samples (round (* +frames-per-buffer+ (/ fm-sample-rate audio-sample-rate))))
                      (fm-samples (get-demodulated-fm-samples device required-fm-samples))
                      ;; 重采样:抽取对应位置的样本
                      (audio-samples (make-array +frames-per-buffer+ :element-type 'single-float)))
                 (loop for i below +frames-per-buffer+ do
                       (setf (aref audio-samples i) (aref fm-samples (round (* i (/ fm-sample-rate audio-sample-rate))))))
                 ;; 转换为双声道(复制单声道到左右声道)
                 (let ((stereo-samples (make-array (* +frames-per-buffer+ 2) :element-type 'single-float)))
                   (loop for i below +frames-per-buffer+ do
                         (setf (aref stereo-samples (* i 2)) (aref audio-samples i)
                               (aref stereo-samples (+ (* i 2) 1)) (aref audio-samples i)))
                   (write-stream astream stereo-samples))))
          ;; 播放结束后清理设备
          (enable-module device (channel-rx 0) nil))))))

额外小贴士

  • 选择合适的bladeRF采样率:不要用40MHz这么高的采样率,不仅浪费资源,重采样也更麻烦。根据FM带宽(通常100kHz-2MHz),选择2-4MHz的采样率足够。
  • 使用专业重采样库:简单抽取会导致音质损失,推荐用cl-resample库实现线性或插值重采样,获得更好的听觉效果。
  • 错误处理:可以在循环里添加超时或错误判断,避免设备异常时程序崩溃。

内容的提问来源于stack exchange,提问作者mwanamutapa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:15:36