Сжатие голоса — гласные звуки

Сжатие голоса — гласные звуки

Want create site? Find Free WordPress Themes and plugins.

Чем меньше битрейт потока данных, тем дальше можно его передать. Если получится сжать голос до 1-2 кБ/с, можно будет общаться на расстоянии до 25км (с использованием трансивера CC1125). Давайте попробуем разработать голосовой кодек самостоятельно, без использования готовых решений вроде codec2.

Как устроен голос? В нём можно выделить несколько элементов:

  • гласные звуки
  • согласные — шумовые — взрывные: п, к, т…
  • согласные — шумовые — фрикативные/свистящие: с, ш, щ, ф, в…
  • согласные — сонорные — дрожащие: р
  • согласные — сонорные — носовые: м, н

Гласные звуки можно рассматривать как музыкальные — это чистый тон + его обертоны различной амплитуды. Согласные звуки — это шумовые звуки с разным тембром и сонорные (тон + шум).

Гласные и согласные здорово различаются по своей информативности, сравните:

гласные: .и..ио.ы .и..ио.ы .и..ио.ы а.ы. .о.

согласные: м.лл..н. м.лл..н. м.лл..н. .л.х р.з

Ирония заключается в том, что гласные звуки очень легко описать небольшим набором параметров. В них нет частот, некратных частоте основного тона, поэтому всё описание сводится до нескольких значений: частота основного тона + амплитуды гармоник. Причём частота доминанты должна быть измерена точно (с точностью до 16 бит), а вот амплитуды гармоник терпят и очень сильную децимацию — поэтому каждую амплитуду можно сжать до 1 байта или даже полубайта.

Давайте начнём с гласных, раз уж их так легко описать. Я разрабатывал алгоритм на Питоне, просто потому что в нём это реально быстро и просто.

Чтение звукового файла

Я записал звук «а» в собственном исполнении и в исполнении девушки, и сохранил его в сыром виде с дикретизацией 8кГц в виде 16-битных знаковых целых с помощью Audacity. Сделаем функцию для чтения такого файла:

from struct import *

def read_file(filename):
f = open(filename, "rb")
chunk = f.read(2)
raw_input = []
while chunk:
raw_input.append(unpack('<h', chunk)[0])
chunk = f.read(2)
f.close()
return raw_input

raw_input = read_file("a_cat_m_8k_200.raw")[00]

Я сохраняю только 800 отсчётов, т.е. 100мс звука — на таком отрезке ещё можно принять сигнал стационарным, неизменяющимся. Следующие фреймы потом проанализируем очередными проходами алгоритма. Построим осциллограмму полученного звука:

import matplotlib.pyplot as plt
plt.plot(cepstrum[0:100], 'r')
plt.grid(b=True, which='major', color='b', linestyle='-')
plt.show()

raw

Построение спектра

Вычислим преобразование Фурье для полученного звука. Фурье возвращает комплексную амплитуду, из которой можно вычислить амплитуду и фазу. Ухо практически нечувствительно к фазе, поэтому мы можем сэкономить половину битрейта, полностью устранив её — оставим только амплитуду, вычислив её функцией abs.

import cmath

def dft(fnList):
pi2 = cmath.pi * 2.0
N = len(fnList)
FmList = []
for m in range(N):
Fm = 0.0
for n in range(N): Fm += fnList[n] * cmath.exp(- 1j * pi2 * m * n / N)
FmList.append(abs(Fm / N))
return FmList

spectrum = dft(raw_input)

plt.plot(spectrum[0:400], 'r')

spectrum

Красивый спектр, правда? Увеличим интересующий нас фрагмент:

spectrum_zoom

Хорошо виден основной тон и его обертоны, спектр имеет периодическую структуру.

Положение каждого пика определяется соотношением: freq = samples / period. Абсцисса пика равна отношению длины фрейма в семплах (в нашем случае 800) к периоду волны.

Правда, есть одна проблема: если взять чистый синус с частотой, не попадающей в выбранную частотную сетку, то после преобразования Фурье он превратится не в дельта-импульс (единичный всплеск), а в какую-то размазанную фигуру:

petal

Чтобы уменьшить этот эффект, необходимо применить к сигналу так называемое «окно». Я умножу сигнал на окно Хэннинга.

hanning

raw_input = [amp * 1.8 * (0.5 - 0.5 * math.cos(2*3.1415*i/len(raw_input))) for i, amp in enumerate(raw_input)]

Красная линия — спектр исходного сигнала, зелёная — спектр сигнала с окном Хэннинга:

spectrum_hanning

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

Построение кепстра

Здесь уже видно всё, что нам нужно, но как программно найти пики и их амплитуды? Проходить обычным поиском локального максимума — неблагодарное дело: пики могут быть слегка сдвинуты, дублированы, и так мы найдём только целую часть периода, а он может быть дробным. Как же нам тогда определить этот период? Тем же способом, как мы определяли периоды в исходном звуковом сигнале, преобразованием Фурье. Только нужно сначала логарифмировать наш спектр, чтобы сильнее выделить пики.

logstrum

И применить к этому преобразование Фурье:

def calc_cepstrum(spectrum):
return dft([math.log(amp) for i, amp in enumerate(spectrum)])
cepstrum = calc_cepstrum(spectrum)
plt.plot(cepstrum[0:100], 'b')

cepstrum

То, что мы построили, называется прикольным словом «кепстр» (Cepstrum) — из названия понятно, что это является чем-то вроде обращения спектра.

Поиск фундаментальной частоты

В нём нам нужен только первый пик, чьё положение будет в точности равно периоду искомого основного тона. Мы найдём этот пик обычным поиском глобального максимума, ограничив область поиска частотами 80..270 Гц, т.к. именно в этом диапазоне лежат основные частоты голосов взрослых людей. В нашем случае это диапазон периодов 30..100 (8000/270..8000/80).

import numpy as np
fund_freq = len(cepstrum) / (np.argmax(cepstrum[30:100]) + 30.0)
print 8000 * fund_freq / len(cepstrum)
> 112.676

В моём случае фундаментальная частота была равна 113Гц, а у моей девушки — ровно 200Гц, что хорошо соотносится с цитатой из Википедии:

Голос типичного взрослого мужчины имеет фундаментальную частоту (нижнюю) от 85 до 155 Гц, типичной взрослой женщины от 165 до 255 Гц.

Запись обертонов

Всё, теперь можно вернуться к спектру, пройти по всем гармоникам найденной основной частоты и записать их амплитуду. Я немного расширяю диапазоны частот обертонов, чтобы найти сумму всего пика (ведь пики размазаны из-за несовпадения сетки частот с основной частотой).

harms = []
for i in range(1, 10): harms.append(int(sum([spectrum[int(round(i * fund_freq)) + j] for j in range (-1, +1) ])))
print harms
> [952, 915, 1111, 674, 803, 898, 988, 879, 463]

Даже девятая гармоника имеет амплитуду, сравнимую с амплитудой основной частоты, поэтому нужно записать довольно много обертонов, минимум 8-10. С другой стороны, нам достаточно ограничиться полосой в 4кГц, более высокие гармоники уже не так нужны для передачи голоса.

Упаковка параметров

Для описания одного фрейма с голосом (10мс в нашем случае) требуется:

  • фундаментальная частота — 2 байта
  • N * амплитуда обертона — N/2 байт

Обычно достаточно не более 10 обертонов, то есть характеристика займёт 2 + 5 = 7 байт. Исходный фрейм весил 1600 байт, то есть степень сжатия составляет ~230 раз. Но нужно понимать, что так мы сжали только гласные звуки, ещё остались согласные — которые, как назло, передают куда больше информации.

Распаковка звука и сравнение

Разжатие звука обратно можно было бы сделать через обратное преобразование Фурье, но сделаем ещё проще — сгенерируем синусоиды нужных частот и амплитуд, и сложим их все друг с другом.

out = []
for i in range(0, 800):
out.append((harms[0] * math.sin(2 * 3.1415926 * i * 1 / (len(cepstrum) / fund_freq)) +
harms[1] * math.sin(2 * 3.1415926 * i * 2 / (len(cepstrum) / fund_freq)) +
harms[2] * math.sin(2 * 3.1415926 * i * 3 / (len(cepstrum) / fund_freq)) +
harms[3] * math.sin(2 * 3.1415926 * i * 4 / (len(cepstrum) / fund_freq)) +
harms[4] * math.sin(2 * 3.1415926 * i * 5 / (len(cepstrum) / fund_freq)) +
harms[5] * math.sin(2 * 3.1415926 * i * 6 / (len(cepstrum) / fund_freq)) +
harms[6] * math.sin(2 * 3.1415926 * i * 7 / (len(cepstrum) / fund_freq))) * 3)
# print out[0:100]
f = open("a_cat_m_8k_200.out", "wb")
for i in range(0, 800):
f.write(pack('<h', out[i]))
f.close()

Откроем в Audacity исходный файл и восстановленный, и сравним осциллограммы:

waveforms

И спектры:

spectrums

Довольно похоже. На слух тоже похоже, причём 10 гармоник звучат гораздо лучше, чем 8.

Did you find apk for android? You can find new Free Android Games and apps.

Читайте также:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *