← Журнал
Jiva Studio

Шумоподавление, которое не стирает голос

Лекции, записанные десятилетия назад, приходят с шипением плёнки, гулом сети и треском. Трудно не убрать шум, а убрать его так, чтобы не съесть голос, не превратить паузы в мёртвую тишину и не выдумать звук, которого никогда не было.

Слушай СадхуEngineeringAudio

Лекции, которые мы публикуем, записали десятилетия назад на кассеты и катушки — в храмовых комнатах и в дороге. Возраст слышен: ровное шипение плёнки, гул сети, треск дешёвого микрофона. Кажется, задача давно решена — запусти шумоподавитель. Но шумоподавитель, который стирает шипение, обычно стирает и часть голоса. Он превращает паузы в неестественный вакуум. А новейшие обучаемые модели ещё и выдумывают звук, которого никто не произносил.

Поэтому правило очистки у нас то же, что и везде: сначала верность источнику. Понижаем шум, не калечим голос, не выбрасываем оригинал.

Как очищается запись

На входе — зашумлённая запись, на выходе — чистая. Между ними очиститель подбирает стратегию под материал, прогоняет её, выравнивает громкость и перекодирует в стандартный моно-MP3 128 кбит/с.

flowchart TD
  IN["Зашумлённая запись"] --> STR{"Выбор стратегии"}
  STR -->|afftdn| A["Шумоподавление через FFT"]
  STR -->|"rnnoise-mix"| R["RNNoise плюс подмешивание по вероятности голоса"]
  STR -->|deepfilternet| D["DeepFilterNet3, обучаемая модель"]
  A --> NORM["Нормализация громкости"]
  R --> NORM
  D --> NORM
  NORM --> OUT["Чистая запись"]

Пять стратегий, один принцип

Единого шумоподавителя нет. Есть пять стратегий на выбор. Каждая — своя точка компромисса между тем, сколько шума уходит, и тем, сколько записи уцелеет.

СтратегияЧто этоЧто даёт
afftdnадаптивный FFT-шумоподавитель из ffmpegмягко понижает фон, без ML, самый быстрый
rnnoiseречевой шумоподавитель RNNoiseагрессивный — может загнать паузы в тишину
rnnoise-mixRNNoise, подмешанный обратно по вероятности голосав паузах остаётся естественный фон
afftdn-rnnoise-mixafftdn → RNNoise → подмешивание истинного оригиналаровный шум усмирён, фон сохранён
deepfilternet (по умолчанию)обучаемый шумоподавитель DeepFilterNet3убирает шипение без лишнего гейтинга

afftdn — классический ход. Это потомок спектрального вычитания: он оценивает спектр шума и вычитает его в частотной области. Фон он понижает лишь на заданную величину и не делает жёсткого гейтинга — безопасно, но мягко:

# afftdn: reduce the noise floor by `nr` dB, then a safety limiter
af = f"afftdn=nr={nr}:nf={nf},alimiter=limit=0.95"   # nr=12 dB, nf=-25 dB

rnnoise сильнее, но на шумном материале он передавливает: паузу для вдоха загоняет в мёртвую, безвоздушную тишину, которая звучит ещё хуже шипения. Лечится это не отказом от него, а тем, что его подмешивают обратно к оригиналу.

По умолчанию стоит deepfilternetобучаемый полнополосный усилитель речи, который работает в реальном времени на обычном CPU. Мы выбрали его вот почему. Ровное шипение плёнки он убирает гораздо лучше afftdn и без артефактов чрезмерного гейтинга, как у RNNoise. И, что важно, это дискриминативная модель, а не генеративная: она подавляет всё, что не голос, но не станет галлюцинировать звук, которого в записи не было. Тот же ужас, от которого мы защищаемся в чате, — модель придумывает то, чего не было, — есть и в звуке. Избегаем его так же: берём инструменты, которые вычитают, а не воображают.

Смесь, которая сохраняет фон комнаты

Самая интересная строка кода борется с передавливанием. Для каждого кадра в 10 мс RNNoise выдаёт и очищенный звук, и вероятность голоса — сигнал детектора речевой активности: около 1 на речи и около 0 в паузе. По нему мы решаем кадр за кадром, сколько оригинала оставить.

Пусть pip_i — вероятность голоса в кадре ii. Долю оригинала, которую мы подмешиваем обратно, задаёт огибающая между двумя границами: в паузах оригинала больше, на голосе меньше:

ei=mmin+(mmaxmmin)pi(mmin=0.10,  mmax=0.25)e_i = m_{\min} + (m_{\max} - m_{\min})\, p_i \qquad (m_{\min} = 0.10,\; m_{\max} = 0.25)

а на выходе — выпуклая смесь оригинального и обработанного сигнала:

yi=eixiorig+(1ei)xiprocy_i = e_i\, x^{\text{orig}}_i + (1 - e_i)\, x^{\text{proc}}_i

В паузе (pi0p_i \approx 0) оставляем 10% оригинала — и тишина сохраняет слабый, правдоподобный фон комнаты вместо вакуума. На голосе (pi1p_i \approx 1) оставляем 25% — и тембр остаётся. Потом огибающую сглаживаем гауссианом, чтобы соотношение смеси не прыгало от кадра к кадру и не рождало музыкальный шум — переливчатый артефакт, который выдаёт небрежный шумоподавитель:

mix_envelope[start:end] = min_mix + (max_mix - min_mix) * voice_prob
mix_envelope = gaussian_filter1d(mix_envelope, sigma=transition_samples / 3)
mixed = mix_envelope * original_float + (1.0 - mix_envelope) * processed_float

Цепочка afftdn-rnnoise-mix идёт ещё дальше: сначала afftdn усмиряет ровный шум, потом RNNoise очищает результат. Но обратно подмешивается истинный оригинал, а не промежуточный afftdn — так что фон в паузах берётся из настоящей записи, а не из её обработанной копии.

До и после

Вот тридцать секунд из лекции по «Шримад-Бхагаватам» 1973 года, а под ней — наш прогон по умолчанию через DeepFilterNet3. На «до» постоянное шипение плёнки заполняет каждый промежуток: фиолетовая дымка размазана по всему верхнему спектру и видна даже в паузах между словами. На «после» этой дымки нет: паузы падают в чистый чёрный, голос звучит чётко и с выровненной громкостью, и ни слова речи не потеряно. Вот вся цель на одной картинке — треск исчезает, голос — нет.

Спектрограммы лекции 1973 года до и после: в оригинале постоянная дымка шипения плёнки заполняет каждую паузу; после DeepFilterNet3 шипение срезано до чистой тишины в промежутках, а гармоники голоса сохранены

Сверху: сырая запись 1973 года — сплошная дымка и есть шипение плёнки. Снизу: после DeepFilterNet3, нашего варианта по умолчанию. Шипение срезано почти до тишины в паузах, а голос сохранён и выровнен по громкости.

Защита пения

Речевой шумоподавитель считает, что на входе речь. Наведи DeepFilterNet на киртану или на санскрит, который читают нараспев, — и он их изуродует: примет мелодию за шум и вычистит начисто. Ранний вариант корпуса так и сделал, и результат вышел непригодным.

Поэтому очистку применяют не вслепую. План склейки размечает участки с пением и распевом и направляет их иначе: пение идёт через мягкий afftdn (или copy — совсем без обработки), а DeepFilterNet проходит только по разговорным фрагментам. Поверх стыков накладывают единый проход громкости, чтобы соединения были не слышны. Шумоподавителю показывают, где музыка, и велят её не трогать.

Честная громкость

Старые записи не просто шумные — они ещё и неровные: где-то шёпот, где-то крик. После шумоподавления громкость выравнивают в два этапа, без накачки: speechnorm мягко приподнимает тихую речь, а loudnorm приводит весь корпус к единому вещательному уровню — в духе EBU R128: интегрированная громкость −16 LUFS и потолок истинного пика −1.5 дБ:

NORMALIZE_FILTER = "speechnorm=e=12.5:r=0.0001:l=1,loudnorm=I=-16:TP=-1.5:LRA=11"

Так что слушатель, переходя от лос-анджелесской лекции 1972 года к вриндаванской 1976-го, не хватается между ними за ручку громкости.

Всегда сохраняй оригинал

Конвейер никогда не перезаписывает источник. Он пишет чистую дорожку рядом с оригиналом, а ползунок «оригинал ↔ чистая» в приложении даёт слушателю в любой момент сдвинуться обратно к сырой записи — тот же порыв, что держать источник в одном касании под каждой переведённой цитатой. В духовных записях артефакт — это ещё и реликвия. Чистая версия — это удобство. Оригинал — сама вещь, и стирать его за слушателя мы не вправе.

Вот что значит «очистка» здесь. Не убрать как можно больше шума — а сохранить как можно больше голоса.


Часть проекта

Слушай Садху

Открыть проект