能不能喂,取决于录制那一刻写进文件的声道数,不取决于模型接口是否接受。手机直接录的单声道文件通常已经把左右两路合并成一路波形,声道间的时间差与电平差基本被抹掉,模型拿到的只是一个声道。MiDashengLM-Spatial 这类带空间描述能力的模型,对没有声道差异的输入一般仍能给出内容层面的判断(谁在说话、有什么声音、大概发生了什么),但左右、前后、方位角、运动轨迹这类描述要么缺失,要么是按语义猜出来的。因此动作顺序是:先用 ffprobe 确认真实声道数和编码参数,再把单声道素材直接试跑一次看输出里有没有方向字段,然后用同一段内容构造只有声道差异的对照样本复跑,最后据此划出「单声道够用」和「必须保留多声道」的边界。
判断依据是录制时的声道数,不是文件名。操作上:用 ffprobe 读 channels 与 channel_layout,同时记录 codec_name、sample_rate、bit_rate;单声道文件先直接跑一次,观察输出里是否出现方位相关描述;再用 pan 滤镜从同一段内容生成左右相同的双声道版本、仅左声道版本、仅右声道版本三份对照样本复跑。适用场景是内容描述、事件识别、声源类别这类任务;方位角、左右定位、运动轨迹必须依赖原始多声道素材。风险边界:单声道输入即使输出了方位词也不要直接采信,必须用左右分离对照验证模型是否真的在读第二个声道。
用 ffprobe 确认文件真实声道数
手机录音常见的坑是文件名或后缀写着立体声,实际容器里只存了一路;也有相反情况,channels 是 2,但两路内容完全相同(dual mono),空间信息同样是零。先看第一条音频流的真实参数:
ffprobe -v error -select_streams a:0 \
-show_entries stream=index,codec_name,channels,channel_layout,sample_rate,sample_fmt,bit_rate \
-show_entries format=filename,format_name,duration,size,bit_rate \
-of json 'data/raw/phone_mono_01.m4a'
字段含义按排查顺序看:channels 是实际存储的声道路数,等于 1 就是单声道,这是最硬的判断依据;channel_layout 是声道布局标签,常见取值 mono、stereo、5.1、5.1(side)、7.1,偶尔为空或 unknown,说明封装时没写布局信息,这时以 channels 为准;codec_name 是编码,aac、opus、pcm_s16le、amr_nb 等,amr_nb 基本只出现在通话录音里,天然单声道;sample_rate 和 bit_rate 要一并记下,后面做对照样本时必须保持一致,否则变量就不止声道一个。
channels 为 2 时再补一步,确认两路是不是同一份内容。把左右相减后看残留电平:
ffmpeg -hide_banner -i 'data/raw/maybe_stereo.m4a' -filter_complex \
'[0:a]pan=mono|c0=c0-c1[diff];[diff]astats=metadata=1:reset=0' \
-f null - 2>&1 | grep -E 'RMS level|Peak level'
相减后 RMS 极低、接近静音,说明两路内容几乎一致,可以当单声道处理。可以先按 -60 dB 粗筛,具体阈值结合素材本身的底噪确认。
建议把结果整理成一张随手可查的记录表,后续每个对照样本都往同一张表里加一行:
文件名 | channels | channel_layout | codec_name | sample_rate | bit_rate | 判断
data/raw/phone_mono_01.m4a | 1 | mono | aac | 48000 | 128k | 真单声道,方位信息不可恢复
data/raw/maybe_stereo.m4a | 2 | stereo | aac | 44100 | 128k | 两路相减近静音,等同单声道
把单声道素材先直接试跑一次
不要凭猜测判断单声道输入会得到什么,先跑一次看实际输出。先确认入口和参数名,不同仓库的脚本名和参数会不一样:
python run_spatial.py `--help`
python run_spatial.py \
`--audio` 'data/raw/phone_mono_01.m4a' \
`--task` describe \
`--out` 'runs/batch01/phone_mono_01_describe.json'
如果是 HTTP 部署,用等价的请求骨架,路由和字段名按实际服务的接口说明替换:
curl -s -X POST 'http://127.0.0.1:8000/v1/spatial' \
-F 'audio=@data/raw/phone_mono_01.m4a' \
-F 'task=describe' \
-o 'runs/batch01/phone_mono_01_http.json'
跑完在输出 JSON 和运行日志里逐项核对下面这些点,把观察结果写进记录表,方便后面和对照组比:
观察点 | 具体看什么 | 本次记录写法
描述文本 | 是否出现 左/右/前/后/方位角/azimuth/behind 这类词 | 只描述内容,无方位词
方向字段 | 是否存在 direction、spatial、azimuth 之类键,值为空还是给了具体角度 | 字段存在但为空
置信度 | 有则记录,仅用于同一批样本横向对照,不作绝对判断 | 0.6x,与对照组比较
日志提示 | 有无 mono、downmix、single channel 之类提示行 | 日志提示输入为单声道
输出了内容判断(有人说话、背景有车声)而没有方位描述,说明这条素材在该任务上只能做内容级问答。输出了具体方位描述,先别高兴,记下数值,等对照组跑完再判定它是不是模型按语义补的。
用同一段内容构造左右分离的双声道对照素材
对照的核心是只变声道结构、内容完全不变。用同一份单声道源文件,派生出三个版本:左右内容相同的双声道、仅左声道有内容、仅右声道有内容。
# 左右相同(L=R),只增加声道数这一个变量
ffmpeg -y -i 'data/raw/phone_mono_01.m4a' -vn -filter_complex \
'[0:a]aformat=channel_layouts=mono,pan=stereo|c0=c0|c1=c0[a]' \
-map '[a]' -ar 48000 -c:a pcm_s16le 'data/ctrl/dualmono_phone_01_48000.wav'
# 仅左声道有内容,右声道置零
ffmpeg -y -i 'data/raw/phone_mono_01.m4a' -vn -filter_complex \
'[0:a]aformat=channel_layouts=mono,pan=stereo|c0=c0|c1=0*c0[a]' \
-map '[a]' -ar 48000 -c:a pcm_s16le 'data/ctrl/leftonly_phone_01_48000.wav'
# 仅右声道有内容,左声道置零
ffmpeg -y -i 'data/raw/phone_mono_01.m4a' -vn -filter_complex \
'[0:a]aformat=channel_layouts=mono,pan=stereo|c0=0*c0|c1=c0[a]' \
-map '[a]' -ar 48000 -c:a pcm_s16le 'data/ctrl/rightonly_phone_01_48000.wav'
pan=stereo|c0=c0|c1=c0 是把同一个输入声道复制到左右两路;0*c0 的写法是为了显式把目标声道清零,避免滤镜把单声道自动上混。命名规范固定为 {结构}_{内容ID}_{采样率}.wav,结构只取 mono、dualmono、leftonly、rightonly 四个值。不要把 L=R 的文件命名成 stereo,否则后面接手的人会误以为是真立体声。
生成后验证两路内容确实一致:分别抽出左、右声道算摘要,两条结果相同就说明只是复制,没有引入其他差异。
for ch in 0 1; do
ffmpeg -v error -i 'data/ctrl/dualmono_phone_01_48000.wav' \
-af "pan=mono|c0=c$ch" -f md5 -
done
对照两组输出划出可用边界
把 mono、dualmono、leftonly、rightonly 四份样本用同一组参数各跑一次,输出分文件保存,方便逐条回溯。判定规则只有一条:比较 leftonly 与 rightonly 两次输出。两个结果的方向描述相同或都为空,说明模型没有把声道差异用于方位判断,此时单声道输出里出现的任何方位词都不可采信;两个结果方向相反、且与声道位置对得上,说明方位判断确实依赖声道,单声道素材就不能用于方位类任务。
任务类型 | 单声道是否够用 | 复现输入 | 输出记录
内容描述、事件识别 | 够用 | data/raw/phone_mono_01.m4a | runs/batch01/phone_mono_01_describe.json
声源类别与数量 | 多数够用,重叠声源需复核 | data/raw/phone_mono_01.m4a | 同上
左右定位、方位角 | 不够用 | data/ctrl/leftonly_phone_01_48000.wav 与 rightonly | runs/batch01/leftonly_*.json、rightonly_*.json
运动轨迹、接近/远离 | 不够用,单声道只有能量变化线索 | data/ctrl/dualmono_phone_01_48000.wav | runs/batch01/dualmono_*.json
距离感、混响判断 | 需谨慎,结论只能作参考 | 三份对照样本对比 | 三个 json 并排比对
每条结论都要能指着具体的输入文件路径和输出 json 路径复述一遍,写不进这张表的结论就先不写,避免后面的人拿着无法复现的说法去做决策。
把素材整理规范固定下来
踩过一次格式和命名的坑就够了。目录按处理阶段分开,原始文件只读不改,归一化和对照样本各自独立:
data/
raw/ # 原始文件,永不覆盖
norm/ # 统一 48000 Hz、pcm_s16le、保留原声道数
ctrl/ # 对照样本:dualmono / leftonly / rightonly
runs/
batch01/ # 每次复跑一个目录,输出文件名与输入文件同名
约定写死四条:文件名用 {结构}_{内容ID}_{采样率}.wav,结构词只用上面四个值;采样率统一 48000 Hz、编码统一 pcm_s16le,避免解码路径不同引入差异;声道数一律保留原样,归一化阶段禁止使用 -ac 1;对照样本必须能从 raw 目录的命令重新生成,命令写在同目录的 notes 里。
整理完用同一个循环复跑一遍,确认命名和声道没有在中途被改掉:
for f in data/norm/*.wav data/ctrl/*.wav; do
b=$(basename "$f" .wav)
ffprobe -v error -select_streams a:0 -show_entries stream=channels,channel_layout \
-of csv=p=0 "$f" | sed "s|^|$b |"
python run_spatial.py `--audio` "$f" `--task` describe \
`--out` "runs/batch01/${b}_describe.json"
done
复跑结果的对账方式是:先看 ffprobe 打印出的声道数是否与文件名里的结构词吻合,再看每个 json 是否都生成了、有没有报错日志。任何一份样本的声道数对不上命名,就回到 raw 目录重新生成,不要在当前文件上就地修补。