之前整理方言语音材料时,最让人头疼的不是录音本身,而是录音之后的标注、声学分析、数据统计和可视化,这几步流程很容易散落在多个软件里,最终导致数据对不上、结果难以复用。如果手工用 Excel 整理,几十个音节的基频数据就能让人耗掉一下午。
所以这篇文章想围绕“方言语音概况”这个主题,给出一套可以复用的技术整理方案。也就是当我们拿到一批方言录音后,如何用 Python 生态工具完成预处理、TextGrid 标注读取、基频提取、共振峰统计、元音图绘制和最终报告输出。文中以江门本地话语语音概况这类研究场景为例,虽然具体方言的音系特征需要实测获得,但整理的流程是通用的。无论你是语言学专业的学生、NLP 方向的开发者,还是对方言保护感兴趣的技术爱好者,都可以按这套流程建立自己的语音分析工作流。
1. 背景与核心概念
1.1 什么是语音概况
语音概况是指对一个方言的语音系统做系统的描写,通常包括声母、韵母、声调、音节结构、语流音变等内容。传统方言学通过田野调查的方式,让发音人朗读字表,调查者用国际音标记下读音,最后归纳出音系表。
从技术角度来说,语音概况可以拆分为几个可计算的任务:
- 音频采集与格式标准化
- 音段切分与标注
- 声学参数提取(基频、共振峰、时长、音强)
- 数据统计与可视化
- 音系归纳与报告输出
把“语音概况”当成一个数据处理项目来做,就可以让整个过程更加透明、可重复、可量化。这也是为什么现在许多方言数据库项目都会公开录音和标注数据。
1.2 为什么选择 Python 来实现
Praat 是语音学研究的经典软件,功能很强,但它不是为批量数据处理而设计的。当你好不容易标注完几十个 TextGrid 文件,想统一提取基频并画图时,手工操作会非常痛苦。
Python 的优势在于它可以把“调用 Praat 分析内核”和“数据处理”结合起来。具体来说:
parselmouth是 Praat 的 Python 封装,可以直接在 Python 中调用 Praat 的语音分析算法。librosa负责音频加载、重采样、滤波等预处理。textgrid可以读取 Praat 的 TextGrid 标注文件。pandas做数据整理,matplotlib做可视化。
这样整个流水线就可以用脚本自动化,不需要反复切换软件。
1.3 江门本地话的案例意义
江门本地话属于粤方言系统的四邑片,地理位置上有自己的特色。对于语音研究者来说,这类方言的声调系统、韵母演变和音节结构都很值得关注。
但这里要强调一点:本文不会也没有办法直接断言江门本地话具体有多少声母、多少个调类。因为任何方言的语音概况都必须基于实测数据,不能靠文献印象或口头传说。更合理的做法是,我们搭建一套从录音到报告的完整处理流程,然后把手头的江门本地话录音导入,跑出属于这份数据自己的结果。
这也正好说明了技术方案的价值:方言语音概况不是一劳永逸的知识点,而是一份需要动态维护和分析的数据资产。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
2.1 软件清单
建议准备以下环境:
- 操作系统:Windows 10/11、macOS 或 Linux 均可,Python 跨平台。
- Python:3.8 及以上,推荐 3.10 或 3.11。
- 语音分析软件:Praat,某些情况下可以直接用 pip 安装的 parselmouth 替代。
- 标注工具:Praat TextGrid,或者 ELAN 导出的 TextGrid 文件。
- 代码编辑器:VS Code 或 PyCharm。
- 录音设备:支持 WAV 格式的录音笔或手机外接麦克风,采样率不低于 44100 Hz。
2.2 Python 依赖安装
建议用虚拟环境隔离项目依赖,避免不同项目的版本冲突。
bash复制python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate
pip install parselmouth librosa numpy pandas matplotlib textgrid soundfile
这里说明一下依赖用途:
parselmouth:提供 Pitch、Formant、Intensity 等声学分析接口。librosa:音频加载、重采样、频谱显示。numpy/pandas:数值计算与表格整理。matplotlib:绘制基频曲线、声学元音图等。textgrid:读取和写入 Praat TextGrid 文件。soundfile:在 Python 中读写 WAV 文件,配合 librosa 使用。
版本方面不需要刻意追求最新,建议以各自库当前稳定版为准。因为语音分析库的 API 偶有调整,如果遇到报错,重点检查 parselmouth 和 textgrid 的版本是否与你参考的教程一致。
2.3 项目目录结构
一个清晰的项目结构会让后期维护容易很多。
code复制dialect-project/
├── audio/
│ ├── raw/ # 原始录音 WAV
│ └── processed/ # 重采样、降噪后的音频
├── annotations/ # TextGrid 标注文件
├── scripts/ # Python 脚本
├── output/ # 图表、统计结果
└── docs/ # 语音概况报告文档
把原始录音和处理后的音频分开存储,是为了保留可追溯的原始数据。如果后续发现处理参数不合理,还可以重新处理,不用重新录音。
3. 方言语音数据采集规范
3.1 发音人选择与授权
采集语音前,必须先获得发音人的知情同意。方言语音数据涉及个人声音特征和语言使用习惯,属于个人信息范畴,尤其是在公开数据集的场景下,需要明确说明数据的用途、范围、保存期限以及是否允许第三方使用。
发音人一般满足以下条件:
- 在当地出生并长大;
- 日常使用该方言交流,不是只会简单词语;
- 发音器官正常,没有严重感冒或声带疲劳;
- 愿意配合长时间录制。
建议记录发音人的性别、年龄段、地方、受教育程度等元数据,但要注意匿名化处理,不要公开可定位到个人的敏感信息。
3.2 录音环境与设备
理想环境是隔音室或安静的室内,避免空调声、风扇声、窗外车流声的干扰。
录音参数建议:
- 采样率:44100 Hz 或 48000 Hz;
- 位深:16 bit 或 24 bit;
- 声道:单声道即可;
- 格式:WAV 无损格式;
- 录音距离:15 到 20 厘米,使用防风噪海绵罩。
录制时建议让发音人使用固定语速、正常音量,每个词条之间间隔 1 到 2 秒,便于后续切分。
3.3 词表设计
词表是语音概况调查的灵魂。为了覆盖方言的声韵调系统,词表通常包含:
- 单音节字表,覆盖不同声母、韵母、声调组合;
- 最小对立对,比如声调对立、送气对立;
- 多音节词表,用于观察连读变调;
- 自然语句,用于观察语流音变。
具体到江门本地话,词表设计需要咨询熟悉该方言的语言学专家,这里不臆测具体音系。
3.4 文件命名规范
推荐使用结构化文件名,例如:
text复制M35_001_za.wav
含义解析如下:
M35:男性,35 岁左右;001:词条编号;za:词条拼音或国际音标缩写,仅作参考。
命名规则需要在项目开始前固定下来,并在文末记录。命名混乱是语音项目后期返工的重要原因之一。
4. 音频预处理实战
4.1 音频加载与重采样
原始录音可能是 44100 Hz,而大部分语音分析任务不需要这么高的采样率。重采样到 16000 Hz 可以减少计算量,同时保留语音分析所需频率范围。
下面是一个完整的预处理脚本。
python复制# 文件路径:scripts/preprocess_audio.py
import os
import librosa
import soundfile as sf
RAW_DIR = "audio/raw"
PROCESSED_DIR = "audio/processed"
TARGET_SR = 16000
os.makedirs(PROCESSED_DIR, exist_ok=True)
def preprocess_file(raw_path, processed_path, target_sr=TARGET_SR):
# load 会把音频重采样到指定采样率,并转为单声道
y, sr = librosa.load(raw_path, sr=target_sr, mono=True)
# 简单幅值归一化,避免不同录音响度差异过大
peak = max(abs(y))
if peak > 0:
y = y / peak
# 写出为 16 bit PCM WAV
sf.write(processed_path, y, target_sr, subtype="PCM_16")
print(f"{os.path.basename(raw_path)} -> {os.path.basename(processed_path)},时长 {len(y)/target_sr:.2f}s")
for fname in os.listdir(RAW_DIR):
if fname.lower().endswith(".wav"):
raw_path = os.path.join(RAW_DIR, fname)
processed_path = os.path.join(PROCESSED_DIR, fname.replace(".wav", "_16k.wav"))
preprocess_file(raw_path, processed_path)
运行方式:
bash复制python scripts/preprocess_audio.py
这段脚本的作用是把 audio/raw 下所有 WAV 文件重采样到 16 kHz,做峰值归一化,然后保存到 audio/processed。峰值归一化不会改变音高,只会把最大振幅调整到 0 dBFS 附近,方便后续统一分析。
4.2 波形和频谱快速检查
预处理之后,可以用一个小脚本快速检查波形是否正常,比如有没有爆音、静音过长、削波等问题。
python复制# 文件路径:scripts/inspect_waveform.py
import librosa
import matplotlib.pyplot as plt
path = "audio/processed/M35_001_16k.wav"
y, sr = librosa.load(path, sr=16000, mono=True)
plt.figure(figsize=(10, 3))
librosa.display.waveshow(y, sr=sr)
plt.title("Waveform")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.tight_layout()
plt.savefig("output/waveform.png")
plt.show()
这段代码依赖 librosa.display,如果使用的是较新版本 librosa,该模块可直接用。如果你发现 librosa.display.waveshow 不存在,可以改用 plt.plot(range(len(y)), y),效果类似。
4.3 静音段切分
如果录音是一条长录音包含多个词条,需要先切分。最简单的做法是用能量检测切分,但这种方法在噪声环境里容易出错。更稳妥的做法是先做端点检测,再人工用 Praat 检查切分点。
这里给出一个基于 librosa.effects.split 的示例。
python复制# 文件路径:scripts/split_silence.py
import librosa
import soundfile as sf
def split_by_silence(input_path, output_dir, top_db=30):
y, sr = librosa.load(input_path, sr=16000, mono=True)
intervals = librosa.effects.split(y, top_db=top_db)
for i, (start, end) in enumerate(intervals, 1):
seg = y[start:end]
out_path = f"{output_dir}/seg_{i:03d}.wav"
sf.write(out_path, seg, sr, subtype="PCM_16")
print(f"{out_path}: {start/sr:.2f}s - {end/sr:.2f}s")
split_by_silence("audio/processed/M35_long_16k.wav", "audio/processed/segments")
top_db=30 表示以最大响度为参考,低于这个值 30 dB 的部分会被视为静音。这个参数需要根据录音环境微调,太大会把字头字尾切掉,太小会保留过长静音。
5. 音段标注与 IPA 转写
5.1 TextGrid 文件结构
Praat 的 TextGrid 是语音标注的标准格式,可以包含多层标注,例如:
sentence:句子层;word:词层;syllable:音节层;phone:音段层。
每层是一个 interval tier,里面是一段一段的时间区间,每个区间有起点、终点和标注文本。标注文本通常使用国际音标(IPA)或方言拼音。
一个最小 TextGrid 的文本结构如下:
text复制File type = "ooTextFile"
Object class = "TextGrid"
xmin = 0
xmax = 1.5
tiers? <exists>
size = 1
item []:
item [1]:
class = "IntervalTier"
name = "phone"
xmin = 0
xmax = 1.5
intervals: size = 2
intervals [1]:
xmin = 0
xmax = 0.62
text = "ts"
intervals [2]:
xmin = 0.62
xmax = 1.5
text = "a"
在实际操作中,一般不需要手写 TextGrid,而是在 Praat 中打开音频后手动标注,也可以使用 Montreal Forced Aligner 之类的工具做自动对齐,再人工修正。
5.2 用 Python 读取 TextGrid
读取 TextGrid 非常简单。
python复制# 文件路径:scripts/read_textgrid.py
import textgrid
tg = textgrid.TextGrid.fromFile("annotations/M35_001.TextGrid")
for tier in tg.tiers:
print(f"层级名称: {tier.name}, 区间数量: {len(tier)}")
phone_tier = tg.getFirst("phone")
for interval in phone_tier:
if interval.mark:
print(f"{interval.minTime:.3f} - {interval.maxTime:.3f} : {interval.mark}")
这里有几个注意点:
getFirst("phone")返回第一个名为phone的层级,如果层级不存在会报错。interval.mark是该区间的标注文本,可能是 IPA 字符。minTime和maxTime都是以秒为单位的浮点数。
5.3 IPA 字符编码问题
国际音标包含许多扩展拉丁字符,比如 ŋ、ʃ、ʔ、ː 等。保存 TextGrid 时,Praat 通常使用 UTF-8 编码,但有时候老版本会使用 ANSI 或系统本地编码。读取时如果出现乱码,可以尝试用文本编辑器将 TextGrid 另存为 UTF-8 编码。
Python 中的字符串操作默认支持 Unicode,所以只要文件编码正确,读取没有问题。写 CSV 时建议加上 encoding="utf-8-sig",否则 Excel 打开会乱码。
python复制import pandas as pd
df = pd.DataFrame([...])
df.to_csv("output/result.csv", index=False, encoding="utf-8-sig")
6. 声学特征提取与可视化
6.1 基频 F0 提取
声调分析的核心是基频(fundamental frequency,F0),也就是声带振动的基础频率,单位是赫兹(Hz)。普通话声调研究中,经常把 F0 曲线做归一化处理后转换成五度值。方言声调分析也是同样的思路。
用 parselmouth 提取一段音频的基频:
python复制# 文件路径:scripts/extract_pitch.py
import parselmouth
import numpy as np
snd = parselmouth.Sound("audio/processed/M35_001_16k.wav")
# time_step=0.01 表示每 10ms 分析一帧
# pitch_floor / pitch_ceiling 是基频搜索范围,女性可调高
pitch = snd.to_pitch(time_step=0.01, pitch_floor=60.0, pitch_ceiling=500.0)
f0_values = pitch.selected_array["frequency"]
times = pitch.selected_array["time"]
# 把无声段(频率为 0)转为 NaN,方便后续统计
f0_values = np.where(f0_values == 0, np.nan, f0_values)
print("帧数:", len(f0_values))
print("有效F0范围:", np.nanmin(f0_values), "-", np.nanmax(f0_values))
参数说明:
time_step:分析窗的时间步长,0.01 秒也就是 10 ms,已经比较精细。pitch_floor:搜索的最低基频,男性语音通常设为 60 到 75 Hz。pitch_ceiling:搜索的最高基频,女性或儿童语音可以设为 500 Hz。
如果提取出的 F0 跳变很大,通常是因为环境噪声或发音不平稳,后面会在常见问题部分详细讨论。
6.2 声调基频曲线绘制
绘制多个音节的 F0 曲线,可以进行声调对比。做法是先把每条 F0 曲线按时间归一化到 0 到 1 之间,再画在同一张图上。
python复制# 文件路径:scripts/plot_f0_curves.py
import parselmouth
import numpy as np
import matplotlib.pyplot as plt
def get_pitch_curve(wav_path):
snd = parselmouth.Sound(wav_path)
pitch = snd.to_pitch(time_step=0.01, pitch_floor=60.0, pitch_ceiling=500.0)
f0 = pitch.selected_array["frequency"]
f0 = np.where(f0 == 0, np.nan, f0)
return f0
files = [
"audio/processed/M35_001_16k.wav",
"audio/processed/M35_002_16k.wav",
]
plt.figure(figsize=(8, 5), dpi=150)
for idx, path in enumerate(files, 1):
f0 = get_pitch_curve(path)
# 去除 NaN,只保留有效段
valid = ~np.isnan(f0)
times = np.arange(len(f0)) * 0.01
plt.plot(times[valid], f0[valid], label=f"item {idx}")
plt.xlabel("Time (s)")
plt.ylabel("F0 (Hz)")
plt.title("F0 Curves of Different Syllables")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("output/f0_curves.png")
这里把多个音节直接画在绝对值时间轴上,虽然能看出曲线走向,但不便于直接比较调形。更规范的做法是先切出每个音节的稳定段,再进行时长归一化。这个优化方向后续可以继续扩展。
6.3 共振峰 F1 / F2 提取
元音音色主要由第一共振峰 F1 和第二共振峰 F2 决定。F1 大致和开口度相关,F2 大致和舌位前后相关。声学元音图通常用 F1 作纵轴、F2 作横轴,并且坐标轴反转,让图上位置与舌位大致对应。
python复制# 文件路径:scripts/extract_formants.py
import parselmouth
import numpy as np
snd = parselmouth.Sound("audio/processed/M35_001_16k.wav")
# 提取前 4 个共振峰
formants = snd.to_formant_burg(max_number_of_formants=4)
# 在音节中段取多个时间点,计算平均 F1、F2
times = np.arange(0.1, 0.5, 0.05)
f1_list = []
f2_list = []
for t in times:
# get_value_at_time 的参数:共振峰编号、时间
f1 = formants.get_value_at_time(1, t)
f2 = formants.get_value_at_time(2, t)
if f1 and f2: # 返回 0 表示该帧没有检测到共振峰
f1_list.append(f1)
f2_list.append(f2)
if len(f1_list) > 0:
print("平均 F1:", round(np.mean(f1_list), 1))
print("平均 F2:", round(np.mean(f2_list), 1))
注意:get_value_at_time 在某个时间点没有检测到共振峰时可能返回 0,所以需要用 if f1 and f2 过滤无效值。
6.4 声学元音图绘制
把多个元音的 F1、F2 均值画到一张图,就得到了声学元音图。
python复制# 文件路径:scripts/plot_vowel_chart.py
import numpy as np
import matplotlib.pyplot as plt
# 示例数据:元音 [i] [a] [u] 的 F1/F2 均值
vowel_data = {
"i": {"F1": 300, "F2": 2200},
"a": {"F1": 850, "F2": 1250},
"u": {"F1": 350, "F2": 700},
}
fig, ax = plt.subplots(figsize=(6, 6), dpi=150)
for vowel, values in vowel_data.items():
ax.scatter(values["F2"], values["F1"], s=80)
ax.annotate(vowel, (values["F2"], values["F1"]), textcoords="offset points", xytext=(10, 10))
ax.invert_xaxis()
ax.invert_yaxis()
ax.set_xlabel("F2 (Hz)")
ax.set_ylabel("F1 (Hz)")
ax.set_title("Vowel Formant Chart")
plt.tight_layout()
plt.savefig("output/vowel_chart.png")
plt.show()
需要注意的是,上面示例中的数值只是演示,不是真实数据。实际项目中要通过测量得到每个元音的 F1、F2 均值,再替换进代码。千万不要把示例数据当成研究结论。
7. 完整项目实战:江门本地话语语音概况整理流程
在这一部分,我们把前面的代码整合成一个可运行的完整流程。假设你已经完成了录音和标注,现在的任务是由一批音频和 TextGrid 文件,生成一份“语音概况统计报告”。
7.1 创建项目结构
bash复制mkdir -p dialect-project/{audio/raw,audio/processed,annotations,scripts,output,docs}
cd dialect-project
7.2 写出数据整理脚本
下面这个脚本会遍历 annotations 目录下的所有 TextGrid 文件,找到 phone 层级,统计每个音段的时长分布,并输出为一个 CSV。
python复制# 文件路径:scripts/analyze_annotations.py
import os
import textgrid
import pandas as pd
ANNOTATION_DIR = "annotations"
OUTPUT_PATH = "output/annotation_stats.csv"
records = []
for fname in os.listdir(ANNOTATION_DIR):
if not fname.endswith(".TextGrid"):
continue
path = os.path.join(ANNOTATION_DIR, fname)
tg = textgrid.TextGrid.fromFile(path)
phone_tier = tg.getFirst("phone")
for interval in phone_tier:
mark = interval.mark.strip()
duration = interval.maxTime - interval.minTime
if not mark:
continue
records.append({
"file": fname,
"phone": mark,
"start": round(interval.minTime, 4),
"end": round(interval.maxTime, 4),
"duration": round(duration, 4),
})
df = pd.DataFrame(records)
df.to_csv(OUTPUT_PATH, index=False, encoding="utf-8-sig")
print(f"共处理 {df['file'].nunique()} 个文件,{len(df)} 个标注区间")
print(df.groupby("phone")["duration"].agg(["count", "mean"]).head(20))
运行:
bash复制python scripts/analyze_annotations.py
预期输出示例:
text复制共处理 15 个文件,320 个标注区间
phone count mean
p 12 0.093 p
...
7.3 结合音频和标注提取每个音段的基频
比较进阶的需求是:根据 TextGrid 里 phone 层的起止时间,截取对应音频片段,计算该音段的平均基频、时长、音强。这样就能得到每一个声母、韵母对应的声学特征表。
python复制# 文件路径:scripts/extract_features_by_interval.py
import os
import textgrid
import parselmouth
import numpy as np
import pandas as pd
ANNOTATION_DIR = "annotations"
AUDIO_DIR = "audio/processed"
OUTPUT_PATH = "output/interval_features.csv"
records = []
def extract_interval_features(sound, start, end):
# 截取该区间音频
segment = sound.extract_part(from_time=start, to_time=end, preserve_times=False)
pitch = segment.to_pitch(time_step=0.01, pitch_floor=60.0, pitch_ceiling=500.0)
f0 = pitch.selected_array["frequency"]
f0 = np.where(f0 == 0, np.nan, f0)
intensity = segment.to_intensity()
db = intensity.values[0]
return {
"f0_mean": round(np.nanmean(f0), 2) if np.any(~np.isnan(f0)) else None,
"f0_sd": round(np.nanstd(f0), 2) if np.any(~np.isnan(f0)) else None,
"duration": round(end - start, 4),
"intensity_mean": round(np.mean(db[db > 0]), 2) if len(db[db > 0]) else None,
}
for fname in os.listdir(ANNOTATION_DIR):
if not fname.endswith(".TextGrid"):
continue
base_name = fname.replace(".TextGrid", "")
tg = textgrid.TextGrid.fromFile(os.path.join(ANNOTATION_DIR, fname))
audio_path = os.path.join(AUDIO_DIR, base_name + "_16k.wav")
if not os.path.exists(audio_path):
print(f"缺少音频: {audio_path}")
continue
snd = parselmouth.Sound(audio_path)
phone_tier = tg.getFirst("phone")
for interval in phone_tier:
if not interval.mark.strip():
continue
features = extract_interval_features(snd, interval.minTime, interval.maxTime)
features["file"] = base_name
features["phone"] = interval.mark
features["start"] = interval.minTime
features["end"] = interval.maxTime
records.append(features)
df = pd.DataFrame(records)
df.to_csv(OUTPUT_PATH, index=False, encoding="utf-8-sig")
print(df.head(20))
这段代码完整展示了“标注驱动”的分析思路。虽然逻辑并不复杂,但省去了一次次在 Praat 里手动截取音频的重复劳动,而且所有参数都记录在脚本里,方便复现。
7.4 生成语音概况汇总报告
最后,我们可以把文本描述、统计表和图片链接组合成一份简单的 Markdown 报告。
python复制# 文件路径:scripts/generate_report.py
import pandas as pd
df = pd.read_csv("output/interval_features.csv")
unique_phones = sorted(df["phone"].unique())
with open("docs/voice_report.md", "w", encoding="utf-8") as f:
f.write("## 语音概况统计报告\n\n")
f.write(f"本次分析共包含 {df['file'].nunique()} 个音频文件,标注了 {len(df)} 个音段。\n\n")
f.write("### 音段清单\n\n")
f.write("、".join(unique_phones) + "\n\n")
f.write("### 音段平均时长\n\n")
duration_summary = df.groupby("phone")["duration"].mean()
f.write(duration_summary.to_markdown() + "\n\n")
f.write("### 基频概览\n\n")
f.write(df[["f0_mean", "f0_sd", "intensity_mean"]].describe().to_markdown() + "\n")
print("报告已生成: docs/voice_report.md")
这里用到了 to_markdown,如果 pandas 版本较低,这个方法可能不可用,可以改成 df.to_string()。
7.5 运行与验证
按顺序运行三个脚本:
bash复制python scripts/preprocess_audio.py
python scripts/analyze_annotations.py
python scripts/extract_features_by_interval.py
python scripts/generate_report.py
整套流程跑完后,output 目录应该有 CSV 统计表和图片,docs 目录应该有报告草稿。这就是一份可以继续加工成论文或博客的原始素材。
8. 常见问题与排查思路
语音分析项目最容易遇到的不是某个算法不会写,而是数据本身的问题,导致分析结果不靠谱。下面整理一张排查表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| F0 提取出很多异常跳变 | 环境噪声大、发音不平稳、搜索范围不合适 | 检查录音环境;适当调整 pitch_floor 和 pitch_ceiling;对 F0 做中值滤波 |
| TextGrid 读取乱码 | 文件编码不是 UTF-8 | 用文本编辑器将 TextGrid 另存为 UTF-8;确认 Praat 版本使用国际音标时保存为 Unicode |
getFirst("phone") 报错 |
TextGrid 中没有名为 phone 的层级 | 用 tg.tiers 打印所有层级名称,确认大小写和拼写 |
| 共振峰提取结果异常 | 分析窗口太大或太小 | 根据语音类型调整 to_formant_burg 的窗口参数,通常女性语音窗口设为 0.003,刻意缩短 |
| 音频切分不准确 | 静音阈值 top_db 不匹配 | 试听切分结果,微调 top_db;必要时人工校正 |
| Excel 打开 CSV 中文乱码 | 保存 CSV 时没有使用 BOM | 使用 encoding="utf-8-sig" 重新保存 |
| 波形显示为一条直线 | 音频被错误归一化或加载失败 | 检查音频文件是否有效,用播放器试听;打印 y.shape 和峰值 |
还有一个隐蔽问题:不同电脑上同一段音频的 F0 提取结果可能略有差异。原因在于 parselmouth 版本更新或音频解码方式差异。建议在报告里记录使用的工具版本,保证结果可复现。
bash复制pip freeze | grep -E "parselmouth|librosa|textgrid|numpy|pandas"
把输出保存到 docs/requirements.txt 或报告附录中。
9. 最佳实践与工程建议
9.1 原始数据永远只读
不要把原始录音和处理后的音频放在同一个目录,更不要直接修改原始录音。所有预处理操作都保存为新文件,这样即使后期发现某个处理参数不合理,也可以回到原始数据重新处理。
9.2 标注规范要提前定义
标注是语音项目中人工成本最高的部分。建议在项目启动前写一份标注规范,明确以下内容:
- 音段层用什么符号集,是国际音标还是方言拼音;
- 如何处理轻声、鼻化、塞音未除阻等特殊现象;
- 标注边界如何处理过渡段;
- 谁来校对每份标注文件。
有了规范,多个标注员之间的差异才会可控。
9.3 每次分析记录参数
语音分析不是点一下按钮就出结果,而是由多个参数决定的。比如基频搜索范围、共振峰数量、分析窗长、是否平滑滤波。建议把关键参数写在一个配置文件里。
yaml复制# 文件路径:config.yaml
audio:
target_sr: 16000
top_db: 30
pitch:
time_step: 0.01
floor: 60.0
ceiling: 500.0
formant:
max_number: 4
window_length: 0.025
Python 里可以用 pyyaml 读取,把参数和数据逻辑分离。
9.4 注意发音人隐私与数据许可
方言录音是宝贵的语言资源,但也是隐私数据。公开发布前必须对发音人做匿名化处理,或者在获得授权后提供受限访问。发布时建议附上数据许可条款,例如禁止用录音训练语音识别系统、不得二次传播等。
9.5 结果可视化避免过度解读
声学测量只提供客观数据,不能自动告诉你某个音属于什么调类。音系归纳还需要结合音位对立、历史比较、母语者听感等证据。所以报告里最好把“测量结果”和“音系归纳”分开写,避免把图表直接等同于语音概况。
9.6 用版本控制管理项目
即使是一个人做研究,也建议用 git 管理所有脚本、标注规范和分析报告。录音文件体积大,可以不进 git 仓库,但要备份到移动硬盘或网盘。每次修改脚本、标注或处理参数时,都留下 commit 记录,这样几个星期后还能知道自己改了什么。
10. 后续可扩展的方向
跑通上面这套流程之后,你已经具备了一个方言语音数据整理的基本工作台。如果想把“语音概况”做成更深入的项目,可以考虑下面几个方向。
- 从单音节字表扩展到语篇录音,分析连读变调和语流音变。
- 用自动对齐工具(例如 Montreal Forced Aligner)批量生成初始标注,再人工修正,提高标注效率。
- 把 F0 曲线归一化后,用聚类算法自动辅助声调调值归纳。
- 建立方言语音数据库,结合地理信息系统绘制方言特征分布地图。
- 把整理好的数据用于语音合成或语音识别研究,帮助方言保护和传承。
语音概况整理看似是一个语言学问题,实际上数据分析方法在其中扮演的角色越来越重。掌握了录音规范、标注读取、声学特征提取和可视化这一整套流程之后,不管换成哪个方言,你都能很快搭出属于自己的分析流水线。
如果你正在计划建立自己的方言语音数据集,建议先从一个发音人、一批单音节字表开始,跑通流程,输出第一张声学元音图和第一批 F0 曲线之后,再逐步扩大样本量。不要一上来就追求大规模采集,流程不稳定时采集越多、返工越麻烦。
