
Полная версия
Цифровая обработка сигналов на Python. От инженера к разработчику.
Прежде чем говорить об обработке, давайте поймём, что мы обрабатываем. Звук — это колебания воздуха. Когда вы говорите, ваши голосовые связки вибрируют. Эти вибрации толкают молекулы воздуха, создавая области повышенного и пониженного давления. Эти области распространяются от вашего рта во все стороны, как круги на воде от брошенного камня. Когда волна давления достигает уха слушателя, она колеблет барабанную перепонку. Колебания барабанной перепонки через систему крошечных косточек передаются в улитку внутреннего уха, где специальные волосковые клетки превращают их в электрические импульсы, которые мозг интерпретирует как звук.
Микрофон работает похоже. Внутри микрофона есть мембрана — тонкая плёнка, которая колеблется под воздействием звуковых волн. Эти колебания превращаются в электрический сигнал: напряжение на выходе микрофона меняется пропорционально звуковому давлению. Если подключить микрофон к осциллографу — прибору, который рисует график напряжения во времени, — мы увидим волну. Эта волна и есть аналоговый звуковой сигнал: непрерывная, плавная линия, которая поднимается и опускается в такт колебаниям воздуха.
У этой волны есть несколько важных характеристик. Первая — амплитуда. Это высота волны, то есть насколько сильно звуковое давление отклоняется от среднего значения. Амплитуда определяет воспринимаемую громкость звука: чем выше волна, тем громче звук. Вторая — частота. Это количество полных колебаний в секунду. Частота определяет высоту звука: чем больше колебаний в секунду, тем выше звук. Частота измеряется в герцах (Гц). Один герц — одно колебание в секунду. Человеческое ухо в среднем слышит звуки от 20 Гц (очень низкий бас) до 20 000 Гц (очень высокий писк). С возрастом верхняя граница снижается — многие взрослые не слышат звуки выше 15 000-16 000 Гц. Третья характеристика — фаза. Это положение волны в конкретный момент времени относительно начала цикла. Две волны одинаковой частоты и амплитуды могут быть сдвинуты друг относительно друга, и этот сдвиг влияет на то, как они складываются.
В реальном мире звук редко состоит из одной частоты. Когда вы говорите, ваши голосовые связки создают сложную волну, в которой смешано множество частот. Когда играет оркестр, звуковое давление в каждой точке пространства — это сумма волн от всех инструментов, отражённых от стен, пола и потолка. Получается очень сложная, но по-прежнему непрерывная волна. И задача цифровой записи — превратить эту непрерывную волну в последовательность чисел, по которой её можно будет точно восстановить.
Дискретизация: делаем непрерывное прерывистым
Цифровой компьютер не может работать с непрерывными сигналами. Компьютер — существо дискретное. Он оперирует отдельными числами в отдельные моменты времени. Чтобы записать звук, нам нужно взять непрерывную звуковую волну и измерить её значение в определённые моменты времени. Этот процесс называется дискретизацией. Мы как бы фотографируем волну много раз в секунду, и каждая фотография — это одно число.
Количество таких измерений в секунду называется частотой дискретизации. Частота дискретизации 44 100 Гц означает, что мы измеряем звуковое давление 44 100 раз в секунду. Почему именно 44 100, а не круглые 40 000 или 50 000? Ответ даёт теорема, открытая Гарри Найквистом и Клодом Шенноном. Она гласит: чтобы точно записать и потом восстановить сигнал, частота дискретизации должна быть как минимум в два раза выше, чем самая высокая частота в этом сигнале.
Человеческое ухо слышит звуки примерно до 20 000 Гц. Значит, чтобы записать всё, что слышит человек, нужна частота дискретизации не менее 40 000 Гц. Разработчики компакт-дисков взяли 44 100 Гц — чуть больше, чем 40 000, с небольшим запасом. Почему именно 44 100, а не 44 000? Это связано с тем, что первые цифровые записи делались на видеомагнитофоны, и частота дискретизации выбиралась так, чтобы быть кратной частоте кадров видео. В Европе это было 25 кадров в секунду, и число 44 100 хорошо ложилось в этот формат. Так техническая необходимость определила стандарт, которым мы пользуемся до сих пор.
Для речи высокая частота дискретизации не нужна. Человеческий голос редко поднимается выше 8 000 Гц, а основные частоты, важные для разборчивости, находятся ниже 4 000 Гц. Поэтому телефонные разговоры оцифровывают с частотой 8 000 Гц — этого достаточно, чтобы понять собеседника, хотя качество заметно хуже, чем у музыки. Для подкастов и аудиокниг часто используют 22 050 Гц — половину от CD-качества. Такой выбор экономит место на диске и ускоряет обработку, не жертвуя качеством восприятия речи.
Давайте проверим всё это кодом. Создадим простой синусоидальный сигнал — чистый тон определённой частоты — и попробуем оцифровать его с разной частотой дискретизации.
python
import numpy as np
import soundfile as sf
import matplotlib.pyplot as plt
# Параметры сигнала
duration = 1.0 # одна секунда
freq = 440.0 # нота "ля" первой октавы
# Создаём непрерывное время с очень высокой частотой дискретизации
# Это наша "идеальная" модель аналогового сигнала
sr_continuous = 441000 # в 10 раз выше, чем CD
t_continuous = np.linspace(0, duration, int(sr_continuous * duration), endpoint=False)
y_continuous = np.sin(2 * np.pi * freq * t_continuous)
print(f"Идеальный сигнал: {len(y_continuous)} отсчётов")
print(f"Частота сигнала: {freq} Гц")
# Теперь симулируем дискретизацию с разными частотами
sample_rates = [8000, 11025, 22050, 44100]
for sr in sample_rates:
# Берём каждый n-ный отсчёт из идеального сигнала
step = sr_continuous // sr
y_sampled = y_continuous[::step]
t_sampled = t_continuous[::step]
print(f"\nЧастота дискретизации: {sr} Гц")
print(f" Количество отсчётов: {len(y_sampled)}")
print(f" На один период сигнала приходится {sr / freq:.1f} отсчётов")
# Сохраняем для прослушивания
sf.write(f'sine_{freq}hz_{sr}.wav', y_sampled, sr)
Этот код создаёт синусоидальный тон частотой 440 Гц — это нота «ля» первой октавы, стандартный камертон. Сначала мы создаём «идеальный» аналоговый сигнал, используя очень высокую частоту дискретизации — в десять раз выше CD. Затем мы имитируем процесс дискретизации: берём из этого идеального сигнала каждый n-ный отсчёт, чтобы получить сигнал с нужной частотой дискретизации. Мы пробуем четыре варианта: 8 000 Гц (телефонное качество), 11 025 Гц (четверть CD), 22 050 Гц (половина CD) и 44 100 Гц (полное CD-качество).
Запустите скрипт и прослушайте полученные файлы. Файл с частотой 44 100 Гц звучит как чистый, гладкий тон. Файл с 22 050 Гц — почти так же, разницу на слух уловить трудно. Файл с 11 025 Гц — звук заметно грубее, появляется лёгкое дребезжание. Файл с 8 000 Гц — тон всё ещё различим, но качество сильно страдает. Это потому, что на каждый период сигнала при частоте 8 000 Гц приходится всего около 18 отсчётов, а при 44 100 Гц — около 100. Чем больше отсчётов на период, тем точнее мы можем восстановить форму волны.
Теорема Найквиста и алиасинг
Теперь давайте проверим саму теорему Найквиста на практике. Теорема утверждает: чтобы точно оцифровать сигнал с частотой F, частота дискретизации должна быть строго больше 2F. Что будет, если попытаться оцифровать сигнал с частотой, которая выше половины частоты дискретизации? Возникнет алиасинг — ложные частоты, которых не было в исходном сигнале.
Представьте себе колесо автомобиля в кино. Вы наверняка замечали, что иногда колеса на экране крутятся в обратную сторону или очень медленно, хотя машина едет быстро. Это и есть алиасинг. Камера снимает с частотой 24 кадра в секунду. Если колесо делает чуть меньше 24 оборотов в секунду, каждый кадр захватывает спицы чуть раньше, чем они завершили полный оборот. В результате нам кажется, что колесо вращается назад. Частота вращения колеса выше половины частоты съёмки — и возникает ложное изображение.
В звуке то же самое. Если частота сигнала выше половины частоты дискретизации, после оцифровки она «отражается» и появляется на более низкой частоте. Эта частота-призрак и называется алиасом.
python
import numpy as np
import soundfile as sf
def demonstrate_aliasing(signal_freq, sample_rate, duration=1.0):
"""
Демонстрирует эффект алиасинга.
signal_freq - частота исходного сигнала
sample_rate - частота дискретизации
"""
nyquist = sample_rate / 2
print(f"Частота сигнала: {signal_freq} Гц")
print(f"Частота Найквиста: {nyquist} Гц")
if signal_freq <= nyquist:
print(" Частота сигнала <= частота Найквиста. Алиасинга нет.")
else:
# Вычисляем частоту алиаса
alias_freq = abs(signal_freq - sample_rate * round(signal_freq / sample_rate))
print(f" Частота сигнала > частота Найквиста! Возникает алиасинг.")
print(f" Ложная частота (алиас): {alias_freq:.1f} Гц")
# Генерируем сигнал на высокой частоте
sr_high = 441000
t = np.linspace(0, duration, int(sr_high * duration), endpoint=False)
y = np.sin(2 * np.pi * signal_freq * t)
# Дискретизируем
step = sr_high // sample_rate
y_sampled = y[::step]
# Сохраняем
sf.write(f'alias_{signal_freq}hz_at_{sample_rate}.wav', y_sampled, sample_rate)
print(f" Файл сохранён: alias_{signal_freq}hz_at_{sample_rate}.wav\n")
# Проверяем на нескольких примерах
demonstrate_aliasing(signal_freq=5000, sample_rate=44100) # должно быть чисто
demonstrate_aliasing(signal_freq=25000, sample_rate=44100) # алиасинг!
demonstrate_aliasing(signal_freq=40000, sample_rate=44100) # сильный алиасинг
Разберём, что делает этот код. Функция demonstrate_aliasing принимает частоту сигнала и частоту дискретизации. Она вычисляет частоту Найквиста — половину частоты дискретизации. Если частота сигнала ниже или равна частоте Найквиста, всё в порядке. Если выше — вычисляется частота алиаса по формуле: abs(signal_freq - sample_rate * round(signal_freq / sample_rate)). Эта формула показывает, на какой частоте появится ложный сигнал.
Первый пример: сигнал 5 000 Гц, дискретизация 44 100 Гц. Частота Найквиста — 22 050 Гц. Сигнал ниже, алиасинга нет. Второй пример: сигнал 25 000 Гц при той же дискретизации. Частота сигнала выше частоты Найквиста. По формуле алиас будет на частоте 19 100 Гц. То есть мы пытаемся записать звук частотой 25 000 Гц, а после дискретизации слышим 19 100 Гц — совсем другой тон! Третий пример: сигнал 40 000 Гц. Алиас будет на частоте 4 100 Гц — высокий тон превратился в низкий.
Запустите скрипт и прослушайте файлы. Первый файл звучит как высокий, но чистый тон. Второй — тон заметно ниже, хотя мы генерировали более высокую частоту. Третий — тон стал басовитым, что совершенно не соответствует исходным 40 000 Гц. Это алиасинг в действии.
Борьба с алиасингом: антиалиасинговый фильтр
Как с этим бороться? Решение простое: перед оцифровкой нужно удалить из сигнала все частоты выше частоты Найквиста. Для этого используется фильтр нижних частот — антиалиасинговый фильтр. Он пропускает частоты ниже частоты Найквиста и подавляет всё, что выше. В реальных аудиоустройствах — звуковых картах, аудиоинтерфейсах — такой фильтр встроен в аппаратуру. При записи звука на компьютер антиалиасинговый фильтр автоматически применяется до дискретизации, и вы никогда не сталкиваетесь с алиасингом в обычной жизни.
Но мы можем смоделировать этот фильтр в коде, чтобы понять, как он работает. В следующих главах мы будем подробно изучать фильтры, а пока используем готовый из библиотеки scipy.
python
from scipy.signal import butter, sosfilt
def anti_alias_filter(y, sr, cutoff=None):
"""
Применяет антиалиасинговый фильтр (фильтр нижних частот).
cutoff - частота среза. По умолчанию = частота Найквиста * 0.9
"""
nyquist = sr / 2
if cutoff is None:
cutoff = nyquist * 0.9 # небольшой запас
# Создаём фильтр Баттерворта 8-го порядка
sos = butter(8, cutoff / nyquist, btype='lowpass', output='sos')
y_filtered = sosfilt(sos, y)
return y_filtered
# Демонстрируем работу фильтра
sr_high = 441000
t = np.linspace(0, 1.0, int(sr_high * 1.0), endpoint=False)
# Создаём сигнал с частотами 5000 и 25000 Гц одновременно
y_mixed = np.sin(2 * np.pi * 5000 * t) + 0.5 * np.sin(2 * np.pi * 25000 * t)
# Дискретизируем без фильтра
step = sr_high // 44100
y_bad = y_mixed[::step]
# Применяем антиалиасинговый фильтр, затем дискретизируем
y_filtered = anti_alias_filter(y_mixed, sr_high, cutoff=20000)
y_good = y_filtered[::step]
sf.write('aliasing_bad.wav', y_bad, 44100)
sf.write('aliasing_good.wav', y_good, 44100)
print("Файлы сохранены. Сравните aliasing_bad.wav и aliasing_good.wav")
print("В плохом файле слышен алиас на ~19100 Гц от сигнала 25000 Гц")
print("В хорошем файле сигнал 25000 Гц подавлен фильтром до дискретизации")
Этот код создаёт смесь двух частот: 5 000 Гц (безопасная) и 25 000 Гц (опасная, выше частоты Найквиста для 44 100 Гц). Без фильтра после дискретизации мы услышим алиас от 25 000 Гц на частоте около 19 100 Гц. С фильтром частота 25 000 Гц подавляется до дискретизации, и мы слышим только чистый тон 5 000 Гц. Прослушайте оба файла и убедитесь сами.
Квантование: превращаем высоту волны в число
Дискретизация решает проблему времени: мы знаем, как часто измерять сигнал. Но есть ещё проблема амплитуды. Звуковая волна непрерывна не только во времени, но и по амплитуде. В каждый момент времени звуковое давление может принимать любое значение в некотором диапазоне. Компьютер не может хранить «любое» значение — он может хранить только конечное количество различных чисел. Процесс округления измеренного значения до ближайшего допустимого числа называется квантованием.
Представьте, что вы измеряете рост человека. Вы можете записать «175 сантиметров», но не «175,384927... сантиметров». Вы округляете до целых сантиметров. Это и есть квантование. В цифровом аудио амплитуда округляется до ближайшего значения, которое может быть представлено заданным количеством бит.
Количество бит определяет, сколько различных уровней громкости мы можем различить. При 8 битах у нас есть 2 в 8-й степени = 256 различных уровней. При 16 битах — 65 536 уровней. При 24 битах — более 16 миллионов уровней. Чем больше бит, тем точнее мы можем записать амплитуду и тем меньше шум квантования — разница между истинным значением и его квантованным приближением.
Шум квантования звучит как тихое шипение, добавленное к сигналу. При 16 битах этот шум очень тихий — около -96 децибел относительно максимального сигнала. При 8 битах шум отчётливо слышен. Давайте смоделируем квантование и послушаем разницу.
python
def quantize(y, bits):
"""
Квантует сигнал до указанного количества бит.
bits - количество бит (например, 8 или 16)
"""
levels = 2 ** bits
# Диапазон от -1 до 1 делим на levels уровней
y_normalized = y / np.max(np.abs(y)) # нормализуем к диапазону -1..1
y_quantized = np.round(y_normalized * (levels / 2 - 1)) / (levels / 2 - 1)
return y_quantized
# Загружаем реальный голос
import librosa
y, sr = librosa.load('voice_sample.wav', sr=44100)
# Квантуем с разной разрядностью
for bits in [16, 12, 8, 6, 4]:
y_q = quantize(y, bits)
sf.write(f'voice_{bits}bit.wav', y_q, sr)
# Вычисляем шум квантования
noise = y - y_q[:len(y)]
noise_power = np.mean(noise ** 2)
signal_power = np.mean(y ** 2)
snr = 10 * np.log10(signal_power / noise_power) if noise_power > 0 else float('inf')
print(f"{bits} бит: отношение сигнал/шум = {snr:.1f} дБ")
print("Файлы сохранены. Прослушайте и сравните качество.")
Прослушайте файлы. 16 бит звучит идеально чисто — это стандарт CD-качества. 12 бит — почти незаметная разница. 8 бит — слышно лёгкое шипение, особенно в паузах. 6 бит — шум становится неприятным, голос искажается. 4 бита — речь ещё можно разобрать, но качество ужасное, как через старую рацию.
Интересный факт: отношение сигнал/шум при квантовании можно оценить по простой формуле: примерно 6 децибел на каждый бит. 16 бит × 6 = 96 дБ — именно столько составляет динамический диапазон CD. 24 бита × 6 = 144 дБ — это уже больше, чем способно различить человеческое ухо. Именно поэтому 24-битная запись считается стандартом для профессиональной студийной работы: она даёт огромный запас по динамическому диапазону, позволяя записывать и очень тихие, и очень громкие звуки без риска клиппинга или потери деталей в шуме.
Связь дискретизации и квантования
Дискретизация и квантование работают вместе. Дискретизация решает, как часто мы измеряем сигнал во времени. Квантование решает, с какой точностью мы записываем каждое измерение. Вместе они определяют качество цифровой записи.
Стандартный аудиофайл формата WAV с параметрами 44 100 Гц, 16 бит, моно хранит 44 100 измерений в секунду, каждое из которых занимает 2 байта (16 бит = 2 байта). Одна минута такой записи занимает примерно 5 мегабайт. Час — около 300 мегабайт. Если использовать стерео вместо моно, размер удваивается. Если использовать 24 бита вместо 16 — увеличивается в полтора раза.
Форматы со сжатием, такие как MP3, используют психоакустические модели, чтобы уменьшить размер файла без заметной потери качества. Они выбрасывают частоты, которые человеческое ухо всё равно плохо слышит, особенно в присутствии более громких звуков на соседних частотах. Это называется маскировкой. Благодаря маскировке MP3 может быть в десять раз меньше WAV, но звучать почти так же. Однако для промежуточной обработки всегда лучше использовать несжатые форматы, потому что каждое пережатие MP3 добавляет новые артефакты.
Практический эксперимент: слышим ли мы разницу
Давайте проведём практический эксперимент, который закрепит всё, что мы узнали о дискретизации и квантовании. Мы возьмём одну и ту же запись, оцифруем её с разными параметрами и сравним.
python
def resample_audio(y, orig_sr, target_sr):
"""Передискретизирует аудио на новую частоту."""
duration = len(y) / orig_sr
new_length = int(duration * target_sr)
t_old = np.linspace(0, duration, len(y), endpoint=False)
t_new = np.linspace(0, duration, new_length, endpoint=False)
y_new = np.interp(t_new, t_old, y)
return y_new, target_sr
# Загружаем качественную запись
y, sr = librosa.load('voice_sample.wav', sr=44100)
print(f"Исходный файл: {sr} Гц, {len(y) / sr:.1f} сек")
# Варианты обработки
configs = [
{'label': 'CD качество', 'target_sr': 44100, 'bits': 16},
{'label': 'Подкаст', 'target_sr': 22050, 'bits': 16},
{'label': 'Телефон', 'target_sr': 8000, 'bits': 8},
{'label': 'Рация', 'target_sr': 8000, 'bits': 4},
{'label': 'Алиасинг (без фильтра)', 'target_sr': 8000, 'bits': 16, 'no_filter': True},
]
for cfg in configs:
y_resampled, new_sr = resample_audio(y, sr, cfg['target_sr'])
# Для варианта с алиасингом не применяем антиалиасинговый фильтр
if not cfg.get('no_filter'):
y_resampled = anti_alias_filter(y_resampled, new_sr)
y_quantized = quantize(y_resampled, cfg['bits'])
filename = f"experiment_{cfg['label'].replace(' ', '_')}.wav"
sf.write(filename, y_quantized, new_sr)
print(f"Создан: {cfg['label']} -> {filename}")
print("\nПрослушайте все файлы и обратите внимание:")
print("- Как меняется качество с уменьшением частоты дискретизации")
print("- Как появляется шум при уменьшении разрядности")
print("- Как алиасинг искажает высокие частоты")
Этот код создаёт пять версий одного и того же аудио. Первая версия — эталонная, CD-качество. Вторая — качество подкаста, половина частоты дискретизации, но 16 бит. Третья — телефонное качество, 8 000 Гц и 8 бит. Четвёртая — качество рации, 8 000 Гц и всего 4 бита. Пятая — умышленно испорченная версия: низкая частота дискретизации без антиалиасингового фильтра. Прослушайте все пять и обратите внимание на характер искажений в каждом случае.
За кулисами: история теоремы Найквиста
Гарри Найквист работал в компании Bell Labs в 1920-х годах. Он занимался проблемой передачи телеграфных сигналов по проводам и обнаружил, что для точной передачи сигнала нужно делать измерения с частотой как минимум вдвое выше, чем самая высокая частота в сигнале. Его работа 1928 года «Certain Topics in Telegraph Transmission Theory» заложила основу для всей цифровой связи.
Клод Шеннон, также работавший в Bell Labs, в 1948 году опубликовал знаменитую работу «A Mathematical Theory of Communication», в которой обобщил и строго доказал результаты Найквиста. Теорема стала называться теоремой Найквиста-Шеннона. Шеннон пошёл дальше и показал, что теорема верна не только для телеграфных сигналов, но и для любых сигналов, включая звук. Эта работа стала фундаментом теории информации и сделала возможной цифровую революцию.
Интересно, что сам Найквист не думал о звуке, когда формулировал свою теорему. Он думал о точках и тире в телеграфных линиях. Но математика оказалась универсальной. Теорема, придуманная для передачи сообщений азбукой Морзе, сегодня обеспечивает работу всего цифрового аудио — от телефонных звонков до стриминговых сервисов.
Лаборатория ошибок
После передискретизации звук стал глухим, без высоких частот. Вы использовали слишком низкую частоту дискретизации и не применили антиалиасинговый фильтр. Высокие частоты отразились и превратились в шум. Всегда применяйте фильтр нижних частот с частотой среза чуть ниже частоты Найквиста перед понижением частоты дискретизации.
После квантования в паузах слышно шипение. Это шум квантования. Вы использовали слишком мало бит. Для чистой записи нужно минимум 16 бит. Если вы работаете с 8-битным аудио — например, из старой игры или архивной записи, — примените шумоподавление после квантования, чтобы убрать шипение в паузах.
Файл после обработки стал занимать неожиданно много места. Проверьте параметры сохранения. Возможно, вы сохранили 8-битный файл с 16-битным заголовком или наоборот. Библиотека soundfile по умолчанию сохраняет WAV с теми параметрами, которые вы указали при создании массива, но лучше явно указывать разрядность.
При попытке загрузить файл с нестандартной частотой дискретизации библиотека выдаёт ошибку или предупреждение. Некоторые библиотеки ожидают только стандартные частоты: 8 000, 11 025, 16 000, 22 050, 44 100, 48 000, 96 000 Гц. Если вы создали файл с нестандартной частотой, некоторые плееры могут отказаться его проигрывать. Придерживайтесь стандартных значений.
Алиасинг не всегда звучит плохо. В некоторых музыкальных жанрах алиасинг используют как творческий эффект — например, в чиптюне и lo-fi музыке. Но для речи и большинства видов контента алиасинг нежелателен. Если вы слышите странные свистящие или звенящие призвуки, которых не было в оригинале, — проверьте частоту дискретизации.
Творческое задание
Запишите свой голос с частотой 44 100 Гц и 16 бит. Затем создайте версии с разными параметрами: 22 050 Гц, 16 бит; 8 000 Гц, 16 бит; 8 000 Гц, 8 бит. Найдите минимальные параметры, при которых ваш голос остаётся разборчивым и приятным на слух. Это знание пригодится, когда нужно будет экономить место или передавать аудио по медленному каналу.
Проведите слепой тест. Дайте другу или коллеге прослушать несколько версий одной записи с разными параметрами дискретизации и квантования. Попросите оценить качество по шкале от 1 до 5. Постройте график зависимости оценки от частоты дискретизации и разрядности. На каком уровне параметров качество перестаёт улучшаться? Это ваш личный порог чувствительности к цифровым артефактам.




