和你在一起的第一年,我还不会写情书
我们都不太会说漂亮话。纪念日那天你问我为什么突然开始写博客, 我说因为有些话写下来比说出来容易——比如这一篇。
当然,也写点 AI、音频技术和日常思考。白天做语音相关的小工具, 晚上读书、跑步、折腾键盘。写下的东西不多,但每一篇都想让她第一个看到。
我们都不太会说漂亮话。纪念日那天你问我为什么突然开始写博客, 我说因为有些话写下来比说出来容易——比如这一篇。
从 AudioContext 到 AnalyserNode,一步一步把麦克风输入画成会跳动的波形, 顺便聊聊采样率和刷新频率之间那些容易踩的坑。第一次跑通时是婷婷在旁边听着。
站点叫听听集,一半是婷婷的名字,一半是我做的事——让机器听懂人的声音。 这篇解释一下这个有点绕的双关,以及我为什么喜欢它。
自回归、非自回归、扩散模型,各有什么取舍?从延迟、音质、可控性三个维度 整理了一份自己的理解,附上几个开源项目的实测数据。
因为它是在十六号被我们接回家的。婷婷说这个名字太随便了, 但叫了半年之后,她自己喊得最顺口。
我负责买菜和洗碗,她负责决定吃什么。这篇记录了我们一年里做得最多的 十二道菜,以及每道菜背后的小事。
整理了传统降噪方法的原理与局限,以及近两年深度学习方案在实时场景下的 实际表现。附一段可以跑起来的 Python 示例。
这个标签下还没有文章。
听听集是我的个人写作空间,创建于 2026 年。名字取自婷婷的「听」, 也取自我每天打交道的「听」——让机器听懂人说话。
这里没有算法推荐,没有广告,也没有评论区。文章按时间排列,你可以按标签筛选、 用搜索框找关键词,或者在归档页按年份浏览。技术文章给同行看,其余的, 大多是写给她一个人看的。
本站使用纯手写的 HTML、CSS 和 JavaScript 构建,没有前端框架,加载速度很快。 代码和文字,都尽量写得干净一点。
有新文章时发一封邮件给你,不定期,不打扰。
按年份倒序排列
文章先保存在这台设备的浏览器里。要让网站访客看到,写完点下方的 「导出 posts.js」按钮,把它放进部署文件夹,再重新上传即可。
我试过好几次给你写信,每次都停在第一句。不是不知道说什么,是太知道说什么了, 反而觉得怎么写都显得轻。
这一年里,我们搬过一次家,养了一只猫,吵过两次很长的架,也在凌晨三点讨论过 「如果以后有了小孩叫什么」。大部分时候,我们过的就是最普通的那种日子: 下班、做饭、刷一会儿手机、睡觉。
你问我纪念日那天为什么突然开始写博客。我说因为有些话写下来比说出来容易。 其实还有一层意思没说:说出来会被风吹走,写下来能留下来。
不是要写得多好,是想让你在很久以后翻到这些字的时候, 还能记得我们现在是什么样子。
所以这个站会有技术文章,会有一些乱七八糟的笔记,也会有写给你的东西。 它们混在一起,才像一个人的生活。
想带你去看一次海,想把厨房的灯泡换掉,想学会做你最爱吃的那道鱼。 都是很小的事。但和喜欢的人一起,小事也值得认真。
第一年结束了。第二年,我们慢慢来。
浏览器里画音频波形,听起来像是个需要大工程的事,实际上 Web Audio API 已经 把最难的部分封装好了。你只需要把 AnalyserNode 接上去,定时读它的数据就行。
getUserMedia 拿到麦克风流 → 包进 AudioContext →
连到 AnalyserNode → 用 requestAnimationFrame 读数据画到 canvas 上。
const ctx = new AudioContext();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
这里有个容易想歪的地方:fftSize 越大,频域分辨率越高,但时间分辨率
会变差。画波形用的是时域数据,fftSize 直接决定每次读多少个采样点。
我一开始把它设成 32768,以为越精细越好,结果画面又慢又糊。后来降到 2048,
配合 requestAnimationFrame,跳动的感觉反而自然了。
不是分辨率越高就越好看。波形的观感来自动态,而不是精度。
第一次跑通的那个晚上,婷婷坐在旁边,说这个跳得挺好看的。 那大概是我今年最喜欢的一句代码评审意见。
站名叫听听集,很多人第一眼以为是个音频播客站。其实是个双关。
婷婷的名字里有个「婷」,取谐音,就是「听」。这是最开始的原因,也是最私人的原因。 这个站从名字开始,就是写给她的。
我白天的工作和语音有关,做的事情说简单点就是让机器听懂人说话。 降噪、转写、合成,都是围绕「听」这件事展开的。
所以「听」这个字对我来说有两层意思:一层是身边这个人,一层是手里这份工作。 把它们放进同一个名字里,我觉得挺合适。
听听集。听听她,也听听世界。
如果你读到这里觉得有点绕,那说明这个双关确实不太成功。但我喜欢它, 喜欢这种只有我们自己才懂的默契。
最近把主流开源 TTS 项目梳理了一遍,大致可以分成三条路线。这篇不写公式, 只写工程上的取舍,以及我实际跑下来的一些感受。
代表是 VITS 一类的工作。音质好,韵律自然,但推理是逐帧生成的,延迟不稳定。 适合离线配音、有声书这种不要求实时性的场景。
FastSpeech 系列的思路,并行生成,延迟低,适合实时交互。代价是韵律的丰富度 不如自回归,长句子里偶尔会出现不自然的停顿。
近两年比较热的方向。音质接近甚至超过自回归,可控性强,但推理步数多, 实时部署对算力要求高。用蒸馏能把步数压下来,不过音质会有一些损失。
没有哪条路线全面胜出。做实时语音助手,我会选非自回归;做内容创作工具, 自回归或扩散更合适。真正决定产品体验的,往往不是模型本身,而是断句、 标点处理和音色库的管理。
模型决定上限,工程决定下限。用户感受到的,永远是下限。
接它回家的那天是十六号。办手续的工作人员问我们想好名字没有,我说还没有, 婷婷在旁边说,那就叫十六吧。
她说完就后悔了,说这名字太随便。但登记表已经填了,改起来麻烦, 就这么叫下来了。半年之后,她自己喊「十六」喊得最顺口,反而我偶尔会喊错。
养猫之前,我以为最难的是打扫和喂食。养了之后才发现,最难的是接受家里 多了一个不受你控制的活物。它会打翻杯子,会在你写代码的时候踩键盘, 会在半夜三点跑酷。
但也是它,让我们第一次有了「我们俩一起照顾一个生命」的感觉。 这种感觉很具体,比任何誓言都具体。
十六不会说话。但它每天蹲在门口等我们回家这件事,比什么话都好听。
我们家分工很清楚:她决定吃什么,我负责买菜和洗碗。这个分工是她定的, 我一开始不太服气,后来发现这样确实效率最高。
这一年里,我们重复做得最多的,反而是最简单的那些:番茄炒蛋、青椒肉丝、 土豆炖牛腩、清蒸鲈鱼、蒜蓉西兰花、冬瓜排骨汤、麻婆豆腐、可乐鸡翅、 酸辣土豆丝、红烧茄子、蛋炒饭、还有周末早上一定会做的煎蛋三明治。
番茄炒蛋是她学会的第一道菜,也是她第一次做给我吃的菜。那天盐放多了, 我们一人喝了两杯水。现在她做得很好,但每次做这道菜,我都会想起那天。
土豆炖牛腩是冬天做的。有一次炖了两个小时,她靠在厨房门口看锅, 说这个味道让她想起小时候。我说那你以后想吃就做,她说好。
菜谱是死的,一起吃饭的人是活的。重要的从来不是那道菜。
降噪是个看起来简单、做起来很深的题目。这篇按时间顺序梳理一下几代方法, 重点写清楚它们各自解决什么问题、又在什么地方力不从心。
最经典的做法。估计噪声的频谱,从含噪信号里减掉。计算量小,实现简单, 但会引入「音乐噪声」——那种像水泡一样的残留杂音。
从统计角度出发,按信噪比动态调整增益。比谱减法平滑,但需要比较准确的 噪声估计,在非平稳噪声下容易失效。
现在主流是时频域掩码估计,或者直接做时域端到端。效果明显好于传统方法, 代价是需要训练数据,模型体积和推理开销也更大。
import numpy as np
def spectral_subtraction(spec, noise_est, alpha=2.0, beta=0.02):
magnitude = np.abs(spec)
phase = np.angle(spec)
cleaned = magnitude - alpha * noise_est
cleaned = np.maximum(cleaned, beta * magnitude)
return cleaned * np.exp(1j * phase)
在实时场景下,我倾向于用轻量深度学习模型做主干,传统方法做兜底。 噪声突变的那一刻,深度学习模型会有短暂的过冲,这时候一个保守的谱减 反而更稳。
降噪的目标不是把噪声消灭干净,而是让人声听起来不费力。