MiDashengLM-Spatial 跑通第一次推理,先对齐声道布局与采样率

文章导读
第一次跑 MiDashengLM-Spatial 的推理,卡住的位置往往不是权重加载,而是喂进去的音频和模型期望的输入对不上:采样率不一致、声道顺序颠倒、整数 PCM 没有归一化。比较省时间的路径是先用一段自己录的多声道音频,把从读文件到拿到输出的最小链路走完,并在中途把声道布局和采样率打印出来核对,对齐之后再去看其他环节。
📋 目录
  1. Ⅰ 用 ffprobe 读出实测声道数、采样率与位深
  2. Ⅱ 把音频转成模型期望的声道数与采样率
  3. Ⅲ 在推理脚本里把音频读成张量并打印形状
  4. Ⅳ 跑一次推理并核对输出与音频片段的对应关系
  5. Ⅴ 把输入参数固定成可复用配置
A A

第一次跑 MiDashengLM-Spatial 的推理,卡住的位置往往不是权重加载,而是喂进去的音频和模型期望的输入对不上:采样率不一致、声道顺序颠倒、整数 PCM 没有归一化。比较省时间的路径是先用一段自己录的多声道音频,把从读文件到拿到输出的最小链路走完,并在中途把声道布局和采样率打印出来核对,对齐之后再去看其他环节。

跑通第一次推理的顺序建议固定为:先用 ffprobe 取音频真实的采样率、声道数和位深,再用 ffmpeg 显式转成模型期望的声道布局与采样率,最后在脚本里打印张量 shape 与 dtype 做断言。任何一步凭播放器界面或听感推断,都容易把声道顺序问题和采样率问题混在一起。模型仓库里标注的期望参数以实际配置文件和报错日志为准,拿不准时先按报错反查,不要猜。

用 ffprobe 读出实测声道数、采样率与位深

播放器状态栏显示的采样率经常是系统混音后的输出参数,不是文件本身的参数;多声道文件在普通播放器里还可能被下混成立体声,界面完全看不出来。所以第一步是在命令行里读文件的真实参数:

ffprobe -v error -select_streams a:0 \
  -show_entries stream=index,codec_name,sample_rate,channels,channel_layout,bits_per_sample,bits_per_raw_sample,sample_fmt,duration \
  -of default=noprint_wrappers=1 input.wav

输出是逐行的 key=value,需要重点记录三个字段:

  • sample_rate:文件的实际采样率,例如 sample_rate=48000。模型侧期望值要和这个值直接比较,不要拿容器或播放器的显示值比较。
  • channels 与 channel_layout:前者是通道数,后者是通道含义。四声道可能是 quad,也可能是 4.0,还可能是环绕声布局,两者含义不同,只记通道数不够。
  • bits_per_sample 或 sample_fmt:PCM WAV 通常给前者,取值 16/24;部分编码只给 sample_fmt=s16/fltp 这类表示。这一项决定后面归一化要不要自己做。

如果 channel_layout 显示为 unknown,说明文件里没写布局信息,这时只能靠录制时的通道顺序手工确认,ffmpeg 也无法自动替你映射。遇到 ambisonic 录音(常标为 4.0 或带 ambisonic 标记),通道顺序是 ACN 还是 FuMa、归一化是 SN3D 还是 N3D,ffprobe 读不出来,必须回到模型侧确认期望格式,这一点靠 -ac 是解决不了的。

把音频转成模型期望的声道数与采样率

拿到实测参数后,转码时不要只用 -ac,因为它只改通道数量,不管通道含义;声道顺序颠倒恰恰是 -ac 之后最容易残留的问题。建议用 pan 滤镜显式写出每个输出通道取哪个输入通道:

# 转成 16 kHz 双声道,并显式声明左右声道来源
ffmpeg -y -i input.wav -ar 16000 \
  -af "pan=stereo|FL=FL|FR=FR" \
  -c:a pcm_s16le data/out_16k_2ch.wav

# 转成 16 kHz 四声道,按 front-left/front-right/back-left/back-right 排列
ffmpeg -y -i input.wav -ar 16000 \
  -af "pan=4.0|FL=FL|FR=FR|BL=BL|BR=BR" \
  -c:a pcm_s16le data/out_16k_4ch.wav

pan 里的输入通道名必须和 ffprobe 读出的 channel_layout 一致,否则 ffmpeg 会报通道名无法解析。如果确认是顺序反了,就把映射写成 FL=FR|FR=FL 这种交换形式,而不是重新录一遍。采样率用 -ar 指定;模型期望 16 kHz 就别保留 48 kHz 交给模型侧下采样,重采样发生在哪一步、用什么算法,出问题时很难定位。

转码完必须复读一遍确认参数已生效,不要假设命令执行成功就等于参数正确:

ffprobe -v error -select_streams a:0 \
  -show_entries stream=sample_rate,channels,channel_layout,sample_fmt \
  -of default=noprint_wrappers=1 data/out_16k_2ch.wav

把这次输出和模型期望值逐项对上,三个字段全部一致再进下一步。如果 channel_layout 仍然和预期不同,说明滤镜没有真正改写布局,回到上一条命令调整 pan 或补 -channel_layout。

MiDashengLM-Spatial 跑通第一次推理,先对齐声道布局与采样率

在推理脚本里把音频读成张量并打印形状

转码正确之后,还要确认代码读出来的张量没有在读取环节被重排。不同库的默认维度顺序不一样:soundfile 返回 (frames, channels),部分工具默认返回 (channels, frames),转置写错会让模型把时间轴当成声道轴。下面是一个可替换路径的骨架,断言位置就是出错时最先报出来的地方:

import soundfile as sf
import numpy as np

WAV_PATH = "data/out_16k_2ch.wav"
EXPECTED_SR = 16000
EXPECTED_CHANNELS = 2

# always_2d=True 保证单声道也是二维,避免 shape 少一维
# dtype="float32" 时 soundfile 已把整数 PCM 归一化到 [-1, 1]
audio, sr = sf.read(WAV_PATH, dtype="float32", always_2d=True)
print("read:", audio.shape, audio.dtype, "sr=", sr)

x = audio.T                      # (channels, frames)
print("tensor:", x.shape, x.dtype)   # 这里看维度顺序是否符合模型要求
assert sr == EXPECTED_SR, f"sample_rate {sr} != {EXPECTED_SR}"
assert x.shape[0] == EXPECTED_CHANNELS, f"channels {x.shape[0]} != {EXPECTED_CHANNELS}"
assert x.dtype == np.float32

# 若是自己用 int16 读入,需要手动归一化,别漏掉这一步
# x = x.astype(np.float32) / 32768.0

管道里传张量给模型的那一行,按仓库实际的调用方式写(通常是 model.generate(x) 或先把张量包一层 batch 维)。如果模型要求 (batch, channels, frames),就在这里补 x = x[None, ...],并把补维后的 shape 再打印一次。打印 dtype 的作用是区分两类问题:dtype 已经是 float32 但数值全在 ±32768 范围内,说明归一化漏了;dtype 是 int16,说明模型侧可能期望浮点。

跑一次推理并核对输出与音频片段的对应关系

最小链路上真正要验证的是输出对应的是刚输入的那段音频,而不是上一次的缓存结果或仓库自带的示例。运行前先记录输入文件的指纹,运行后在日志里对照:

md5sum data/out_16k_2ch.wav
ls -l `--time-style`=full-iso data/out_16k_2ch.wav
python infer.py `--audio` data/out_16k_2ch.wav    # 入口按仓库实际脚本替换

日志里值得盯的观察点有四个:模型与设备加载那一行(确认没走 CPU 回退或旧权重)、输入 shape 与 dtype(和第 3 步打印的值一致)、输出文本或坐标结果、以及这次运行的输出文件路径。如果输出写死成同一个文件名,重复运行会覆盖,肉眼看着结果“没变”其实只是没刷新,建议输出文件名带上时间戳或输入文件名的哈希。

换一段音频复跑对比是最直接的判据:录两段内容差异明显的音频,比如一段报数、一段带明显方向性的声音,用同一套配置各跑一次。输出随输入变化,说明链路是通的;两次输出几乎相同,优先怀疑读到了缓存文件、路径写死或音频被截断成静音,而不是先怀疑模型。核对片段对应关系时,可以把输入音频切出前几秒单独跑一次,看输出是否只描述这段内容,这样能排除模型实际读的是整段或另一段音频。

把输入参数固定成可复用配置

链路走通之后,把路径、采样率、声道数从代码里挪出来,后续换音频只改路径。下面是一份最小配置,字段名可以按仓库习惯调整,但三个关键项要保留:

audio:
  path: data/out_16k_2ch.wav
  sample_rate: 16000
  channels: 2
  channel_layout: "FL,FR"   # 记录含义,转码命令按它填 pan
  dtype: float32

脚本读配置后仍保留第 3 步的 assert,配置和文件实际参数不一致时直接失败,而不是让形状错误一路带到模型里变成难读的报错。一次改配置后的复跑验证方法是:只改 path,其余字段不动,跑完检查三件事——打印出的 shape 和 dtype 与改动前一致、输入文件 md5 是新的、输出内容跟着新音频变化。三项都满足,说明配置项确实生效;若 shape 变了,说明新文件的声道数或采样率与配置不符,先回到 ffprobe 那一步核对,再决定是转码还是改配置。声道布局这类无法从文件里读出的含义(例如 ambisonic 的通道顺序约定),建议作为注释写在配置旁边,避免换人接手时重新踩一遍。