Перейти ко второй части
Введение
Во второй половине 90-х я увлеченно знакомился с компьютерной музыкой на PC и пытался сочинять свои композиции в популярных тогда программах-трекерах, таких как Fast Tracker II и EdLib. Но наибольшее впечатление на меня в те времена произвел аудиоязык Csound, о котором я узнал, благодаря статье 1998 года из журнала «Компьютерра». Для работы с Csound нужен только текстовый редактор. Быстродействие компьютера, наличие библиотек сэмплированных звуков или качество звуковой карты не имели особого значения. Это был программный синтез звука в чистейшей его форме.
На современных персональных компьютерах не составляет труда реализовать впечатлившие меня когда-то музыкальные примеры для Csound уже в реальном времени. Сегодня специализированные аппаратные решения для задач синтеза звука используются музыкантами все реже, и наибольшую популярность приобрели аудиоплагины. Так дело, разумеется, обстояло далеко не всегда. Какими были первые попытки создания программного синтеза на ранних персональных компьютерах? Что это были за компьютеры? Наконец, каковы детали реализации ранних методов программного синтеза? На эти вопросы я постараюсь ответить далее.
Практика
На мой взгляд, обучение в историческом контексте имеет много плюсов и поэтому, наряду с описанием дел давно минувших дней, мне захотелось побудить читателей, не чуждых программированию, к практическому звуковому дизайну. Я не стану углубляться в теорию, поскольку вижу своей задачей выработать у интересующихся читателей начальное и интуитивное представление о том, как же программируются синтезаторы. По этой причине примеры, которые будут показаны далее, реализованы без типичных трюков и хитростей, знакомых бывалым разработчикам.
Мы будем работать в духе пионеров компьютерной музыки 60-х, которые использовали в своих исследованиях серию аудиоязыков MUSIC N, разработанную Максом Мэтьюсом (Max Mathews). Csound, о котором шла выше речь, является одним из многочисленных потомков MUSIC N. Разумеется, у нас есть определенные преимущества перед первопроходцами: не нужно ждать компиляции кода долгими часами, не нужно записывать результат на магнитную ленту, а затем отправляться с ней в другой город, чтобы на еще одном компьютере перевести полученные данные в звук.
Примеры синтеза звука будут даны на языке Питон (Python). Одним из важных плюсов этого языка является схожесть его синтаксиса с псевдокодом, чтение которого не должно вызвать особенных трудностей даже у читателей, не знакомых с Питоном. Предлагаемые в статье программы служат чисто иллюстративным целям и призваны сподвигнуть читателя экспериментировать со звуковыми алгоритмами. Каждый пример является отдельной программой для 2 и 3 версий языка и зависит только от стандартных библиотек. Запуск примеров заканчивается формированием wav-файла. Использование PyPy позволит ускорить вычисления в несколько раз.
Для сохранения результата в wav-файл я применяю стандартный модуль wave. Все дальнейшие примеры предваряются кодом, который показан ниже.
SR = 44100
# Запись массива 16-битных сэмплов в режиме моно
# с частотой дискретизации SR в wav-файл
def write_wave(filename, samples):
f = wave.open(filename, "w")
f.setparams((1, 2, SR, len(samples), "NONE", ""))
f.writeframes(b"".join(
[struct.pack('<h', round(x * 32767)) for x in samples]))
f.close()
# Перевод секунд в количество сэмплов
def sec(x):
return SR * x
Вот простой пример, в котором используются лишь пары синусоид.
# Сумма синусоид, имеющих частоты, определенные в bank
def sines(bank, t):
mix = 0
for f in bank:
mix += math.sin(2 * math.pi * f * t / SR)
return mix
# DTMF-сигналы для цифр 1-9
DTMF = [
[697, 1209], [697, 1336], [697, 1477],
[770, 1209], [770, 1336], [770, 1477],
[852, 1209], [852, 1336], [852, 1477]
]
samples = []
# Цикл по цифрам телефонного номера
for d in [3, 1, 1, 5, 5, 5, 2, 3, 6, 8]:
for t in range(int(sec(0.05))):
samples.append(0.5 * sines(DTMF[d - 1], t))
for t in range(int(sec(0.05))):
samples.append(0)
write_wave("dtmf.wav", samples)
Исходный код
Звучание
Здесь генерируется звук тонального набора для фиктивного телефонного номера 3115552368. Цифры номера в формате DTMF закодированы с помощью пар синусоид, частоты которых заданы в массиве DTMF. Обратите внимание на характерные щелчки в звучании, которые являются результатом резких перепадов амплитуды. Избавиться от подобных щелчков можно с помощью амплитудной огибающей или НЧ-фильтра. Об использовании огибающих и фильтров подробнее будет рассказано далее.
TWANG
И они, на самом деле, показали мне три вещи. Но я оказался настолько поражен первой из этих вещей, что не обратил внимание на остальные. Помимо прочего они показали мне объектно-ориентированное программирование, на что я не обратил внимание. Еще они показали мне вычислительную сеть… в которую были объединены больше сотни компьютеров Alto, там была электронная почта и т.д., и т.д. Всего этого я не заметил, поскольку оказался полностью ослеплен первым, что мне было показано — графическим интерфейсом пользователя.Так, спустя время, Стив Джобс вспоминал о своем визите в научно-исследовательский центр Xerox PARC в 1979 году. Компьютер Xerox Alto, который был создан еще в 1973 году, во многом предвосхитил облик персональных компьютеров следующих десятилетий. Автором концепции Alto является ученый Алан Кэй (Alan Kay), который не чужд и музыке: в молодости он успел побывать джазовым гитаристом, а в зрелых годах освоил и церковный орган. Возможно, по этой причине Alto имел не только выдающиеся для своего времени графические возможности, но и хорошую поддержку звука.
В 1975 году 16-битный Alto, который функционировал на частоте 5.88 МГц и обладал 128 Кбайт оперативной памяти, уже имел и некоторое музыкальное ПО. В частности, для данного компьютера существовала программа TWANG, разработанная Тэдом Каэлером (Ted Kaehler) на языке Смолток (Smalltalk). В графическом виде, вполне в духе современных музыкальных MIDI-редакторов, демонстрировался поток нотных событий, которые могли быть добавлены с помощью мыши или с клавиатуры электрооргана. Звук формировался в реальном времени методом FM-синтеза, причем каждый параметр тембра, представленный в виде графика, можно было регулировать, опять же, мышью. Рисование пользователем огибающих с произвольным количеством сегментов, а также наглядное представление всех параметров FM-синтеза на едином графике не всегда можно встретить даже в современных программных синтезаторах.
Просмотр и редактирование нотных событий в TWANG
FM-синтез был открыт в конце 60-х музыкантом и ученым Джоном Чоунингом из Стэнфордского университета во время его экспериментов с одним из ранних аудиоязыков серии MUSIC N. Чоунинг заметил, что при использовании обычного эффекта вибрато (который представляет собой периодическое изменение высоты звука) на достаточно высокой частоте, тембр модулируемого сигнала начинает существенно меняться. Наибольший интерес с точки зрения слушателя представляет не статичный спектр, а различные его видоизменения, разворачивающиеся во времени. В этом смысле FM-синтез имеет большие возможности при скромных вычислительных требованиях, поскольку, управляя амплитудой и частотой модулятора, легко динамически влиять на спектр несущего сигнала.
Редактирование параметров FM-синтеза в TWANG
Кстати говоря, благодаря средствам, вырученным Стэнфордским университетом от продажи в начале 70-х лицензии на FM-синтез компании Yamaha, при университете был создан исследовательский центр CCRMA (Center for Computer Research in Music and Acoustics). С именем этого центра связаны многие важные результаты в области методов синтеза звука.
Известны параметры FM-синтеза, реализованного на Xerox Alto:
- 5 голосов (по 2 осциллятора на каждый), работающих в реальном времени,
- 12-битные сэмплы с частотой дискретизации 13.7 кГц,
- Обновление параметров голосов на частоте 60 Гц.
Примечательно, что никакого специального оборудования, кроме ЦАП, для работы со звуком в Alto не использовалось. FM-синтез был реализован программно и производился одновременно с обработкой GUI и прочими задачами. Каким же образом скромный по возможностям компьютер справлялся с синтезом в реальном времени? Дело в том, что в Alto программисту позволялось прямо в процессе работы компьютера обновлять микрокод. Работа на уровне микрокода широко применялась разработчиками Xerox PARC для управления внешними устройствами, а также для виртуализации набора инструкций и создания проблемно-ориентированных команд, например, для работы с графикой. В компьютере существовала также аппаратная поддержка кооперативной многозадачности для процессов, выполнявших микрокод.
Пример работы в TWANG
Для вывода звука в TWANG использовались специальные инструкции. Соответствующий микрокод для реализации FM-синтеза были добавлен Стивом Сондерсом (Steve Saunders). Он сумел обойтись в своей реализации без операций умножения при помощи двух простых приемов: с помощью замены синуса треугольным сигналом в модуляторе, а также используя тригонометрическое тождество sin(x + a) + sin(x — a) = 2 * cos(a) * sin(x) для управления амплитудой. Интересно отметить, что компания Yamaha позже по-своему избавилась от операций умножения в своих FM-чипах с помощью log- и exp-таблиц.
Пример звучания TWANG
Программа TWANG дальнейшего развития не получила. Сондерс в начале 80-х приложил руку к разработке звукового чипа Atari Amy. Данный чип представлял собой достаточно мощный аддитивный синтезатор для микрокомпьютеров, опережающий по качеству звучания типичные звуковые генераторы того времени, но неурядицы в компании Atari не позволили Amy увидеть свет.
Практика
Сразу уточню, что далее речь пойдет о фазовой (PM), а не частотной модуляции, хотя использоваться будет второе именование. Именно вариант с модуляцией фазы реализован фирмой Yamaha в своих FM-синтезаторах.
В примерах этого раздела будет удобно использовать форму описания генератора синусоиды в виде объекта, который хранит текущую фазу и имеет метод next для выдачи очередного сэмпла. Здесь не используется явно параметр t, как это было в примере с DTMF-сигналами, и значение фазы меняется в пределах периода функции sin.
class Sine:
def __init__(self):
self.phase = 0
# Выдать очередное значение синусоиды с частотой freq и смещением фазы pm,
# обратите внимание, что использование частотной, а не фазовой модуляции
# отразилось бы на аккумуляторе фазы phase, что не всегда желательно
def next(self, freq, pm=0):
s = math.sin(self.phase + pm)
self.phase = (self.phase + 2 * math.pi * freq / SR) % (2 * math.pi)
return s
В FM-синтезе осцилляторы объединяются в различные конфигурации на основе следующих базовых соединений: последовательного, параллельного (аддитивный синтез), и с обратной связью. Даже используя всего два осциллятора, соединенных последовательно, можно получить достаточно сложные тембры. В этом случае вывод одного из осцилляторов подается на вход смещения фазы другого осциллятора. С точки зрения создания тембров такой вариант соединения можно приближенно рассматривать как пару осциллятор-фильтр в аналоговом синтезаторе. Ниже показан пример реализации последовательного соединения для синтеза простейшего тембра колокола.
# FM-синтез по формуле
# y(t) = Ac * sin(2 * PI * fc * t + Am * sin(2 * PI * fm * t))
oc = Sine()
om = Sine()
samples = []
for t in range(int(sec(1))):
env = 1 - t / SR
samples.append(0.5 * oc.next(80, 3 * env * om.next(450)))
write_wave("bell.wav", samples)
Исходный код
Звучание
В данном примере осциллятор om («модулятор») управляет звучанием основного осциллятора oc («несущей»). За характерный тембр колокола отвечают следующие параметры:
- соотношение частот модулятора и несущей, создающее негармонические обертона,
- линейное затухание амплитуды модулятора (3 * env), которое вносит нужную динамику в спектральный состав.
Довольно сложно дать краткий рецепт по выбору параметров для синтезирования FM-тембров. Теоретические сведения на этот счет можно почерпнуть в книге FM Theory & Applications. Но, зачастую, важнее оказываются интуиция и практика. В целом, в звуковом дизайне базовым подходом является декомпозиция сложного тембра, разбиение его на отдельные слои и элементы таким образом, чтобы над ними можно было работать практически независимо.
Для удобной работы с управляющими сигналами, которые динамически меняются, как, например, это происходит с амплитудой модулятора в данном случае, полезно реализовать генератор огибающей. Его код показан ниже.
# Кусочно-линейная функция, используется уравнение прямой по двум точкам
def linear_env(segs, t):
x0 = 0
y0 = 0
for x1, y1 in segs:
if t < x1:
return y0 + (t - x0) * ((y1 - y0) / (x1 - x0))
x0, y0 = x1, y1
return y0
class Env:
def __init__(self, segs):
self.segs = segs
self.phase = 0
def next(self, scale=1):
s = linear_env(self.segs, self.phase)
self.phase += scale / SR
return s
Огибающая определяется с помощью массива линейных сегментов. Координата x обычно задает время, а координата y может трактоваться по-разному, например, как амплитуда или частота.
Следующий пример на тему FM-синтеза и огибающих связан с моделированием ударных. Здесь реализованы тембры бас-бочки (kick) и рабочего барабана (snare). Обратите внимание, что в коде синтеза рабочего барабана используется соединение осцилляторов с обратной связью (переменная fb). Использование обратной связи в FM-синтезе (это было изобретение компании Yamaha) позволяет создавать большое разнообразие тембров простыми усилиями. С ее помощью возможно генерирование шума, как в данном случае, а также создание пилообразной и прямоугольной звуковых волн. Многочисленные генераторы огибающей используются в данном примере не только для контроля амплитуды, но и частоты осцилляторов.
def kick(samples, dur):
freq = 100
o1 = Sine()
o2 = Sine()
e1 = Env([(0, 1), (0.03, 1), (1, 0)])
e2 = Env([(0, 1), (0.01, 0)])
for t in range(int(sec(dur))):
o = o1.next(freq * e1.next(2.5), 14 * e2.next() * o2.next(freq))
samples.append(0.5 * o)
def snare(samples, dur):
freq = 100
o1 = Sine()
o2 = Sine()
e1 = Env([(0, 1), (0.2, 0.2), (0.4, 0)])
e2 = Env([(0, 1), (0.17, 0)])
e3 = Env([(0, 1), (0.005, 0.15), (1, 0)])
fb = 0
for t in range(int(sec(dur))):
fb = e2.next() * o1.next(freq, 1024 * fb)
samples.append(0.5 * o2.next(e1.next() * freq * 2.5, 5.3 * e3.next() * fb))
samples = []
for i in range(4):
kick(samples, 0.25)
kick(samples, 0.25)
snare(samples, 0.5)
write_wave("drums.wav", samples)
Исходный код
Звучание
В целом, наиболее реалистичные тембры получаются в FM-синтезе при использовании 6 и более осцилляторов, но оперирование такими конфигурациями лежит за пределами возможностей обычных электронных музыкантов, поэтому в настоящее время данный вид синтеза чаще всего сочетается с другими подходами.
SAM
В конце 70-х популярность начали приобретать специальные чипы для синтеза речи. Среди первых была микросхема TMS5100, которая использовалась в популярной детской игрушке Speak & Spell. Разработка данной микросхемы потребовала от инженеров Texas Instruments недюжинных усилий и ее появление на свет считается важной вехой в развитии процессоров для цифровой обработки сигналов. На этом фоне кажется особенно удивительной история S.A.M. (Software Automatic Mouth) — полностью программного синтезатора речи, который появился на свет еще в 1979 году и к началу 80-х был реализован на простейших 8-битных микрокомпьютерах компаний Apple, Atari и Commodore.
Реклама SAM
SAM представлял собой набор программ без какого-либо графического интерфейса. C помощью этих программ можно было сразу переводить произвольный текст в речь или, с использованием фонетического алфавита, предварительно описать в деталях способ озвучивания текстов синтезатором. Разборчивость фраз, на мой взгляд, у SAM не уступает чипам от Texas Instruments. Более того, в варианте формантного синтеза, реализованного в SAM, возможно на лету менять тембр и интонацию голоса, которым произносятся пользовательские фразы.
Что такое формантный синтез? Самым основательным подходом к синтезу голоса являются решения на основе физического моделирования, отличающиеся как вычислительной сложностью, так и сложностью настройки. Более простой способ состоит в использовании генераторов тоновых и шумовых звуков, которые обрабатываются набором полосовых фильтров, имитирующих форманты (пики в спектре сигнала, которые определяют звуки речи). В случае Speak & Spell готовые фразы были закодированы на основе линейного предсказания (LPC). Соответствующие параметры определяют работу тонового и шумового генератора, а также общего для них цифрового фильтра 10-го порядка.
Вычисление очередного значения на выходе фильтра Speak & Spell требовало 20 операций умножения, что явно находилось за пределами возможностей микрокомпьютеров того времени. Какой же трюк использовали создатели SAM? Судя по всему, этот вопрос не давал покоя исследователям долгие годы, пока, наконец, в середине 2000-х код SAM для микрокомпьютера Commodore C64 не был дизассемблирован и переведен на язык Си.
Как выяснилось, реализация синтеза речи в SAM обошлась вообще без единой операции умножения. Для порождения формант в коде фигурируют только генераторы синусоиды и прямоугольного сигнала. Вообще говоря, существует синтез синусоидальных волн, в котором формирование формант попросту заменяется синусоидами на соответствующих частотах, но этот вариант не отличается разборчивостью речи. Подход SAM иной. Его истоки находятся теории вейвлетов и гранулярного синтеза. Существует множество разновидностей этого подхода, но основным их признаком является имитация эффекта работы полосового фильтра. Поскольку «фильтр» является фиктивным, то невозможно и подать на него какой-то сторонний сигнал. Вместо сложной реализации настоящих полосовых фильтров синтез формант осуществляется на уровне порождения «всплесков» или «гранул» волновых форм. Подобным образом имитируется работа фильтров в синтезаторах Casio CZ, Roland MT-32 и Yamaha FS1r. Одной из наиболее известных разновидностей этого подхода является FOF (Формантная Волновая Функция), которая успешно себя показала в синтезе пения.
Позволю себе небольшое отступление. Добрая треть фильма «Стив Джобс» (2015) посвящена чрезмерно драматическому изображению событий, связанных с проблемами запуска синтезатора речи во время презентации первого Macintosh, происходившей в 1984 году. О том, как дело обстояло в реальности, можно узнать из воспоминаний Энди Херцфельда (Andy Hertzfeld), одного из ключевых разработчиков Apple того времени. Стив Джобс был ценителем музыки, как классической, так и популярной. Очевидно, по этой причине, а также под впечатлением от передовых компьютеров Xerox 70-х, в Macintosh была реализована звуковая подсистема неплохого, по тем временам, качества: моно, 22 КГц, 8 бит. Джобсу было важно, чтобы на презентации компьютер проиграл торжественную мелодию из «Огненных колесниц» (Chariots of Fire) Вангелиса. Увы, время поджимало, и Херцфельд успел синтезировать тембры лишь на уровне DTMF-сигналов, которые мы с вами реализовали в первом примере. В этой ситуации команду Macintosh выручил Марк Бартон (Mark Barton), не кто иной, как автор SAM. Разумеется, Джобс был в восторге от идеи, что компьютер представит себя сам. Так оно и произошло. Вот только «Огненные колесницы» запустили во время презентации с CD-проигрывателя, а модель компьютера 128K пришлось ради синтезатора речи тайно заменить на 512K, которая была еще в разработке. Впрочем, это уже другая история.
Версия SAM для Macintosh под названием MacinTalk, в отличие от микрокомпьютерных вариантов, генерировала более качественный 8-битный звук. Компания SoftVoice, производитель SAM, со временем выпустила версии синтезаторов речи для Amiga и Windows. К настоящему моменту, судя по всему, SoftVoice больше не существует, но популярность восстановленного исходного кода SAM растет среди разработчиков встраиваемых систем, которые желают реализовать простой синтез речи на ограниченных по ресурсам микроконтроллерах.
Практика
Из изучения кода SAM видно, что данный синтезатор речи имеет лишь 4 осциллятора:
2 генератора синусоиды, генератор прямоугольного сигнала (меандра), а также генератор импульсов, имитирующий работу голосовых связок. Параметры этих осцилляторов закодированы в потоке фреймов, которые обновляются с определенной частотой, соответствующей скорости «проговаривания» фраз.
Генератор меандра можно реализовать самым примитивным образом с помощью тернарного условного оператора (в синтаксисе языка Си это выглядит так: phase < 0.5? 1: -1). Данный вариант, вообще говоря, страдает одним очень распространенным недостатком из мира цифровой обработки сигналов: наложением частот (aliasing), поскольку идеальный меандр имеет бесконечный спектр. Но в звуковых волнах не должны присутствовать частоты, большие или равные половине частоты дискретизации (теорема Котельникова), иначе мы рискуем получить достаточно различимую на слух «грязь» в спектре. Различным способам достижения корректного спектра сигнала для простейшего «квадрата» или «пилы» можно посвятить целую книгу. Одним из простых трюков является использование FM-синтеза с обратной связью. Сейчас же нам будет достаточно и «неправильного» способа. Я поставил слово «неправильный» в кавычки еще и потому, что алгоритмы музыкального синтеза это не совсем то же самое, что и обычная цифровая обработка сигналов. В музыке важен результат с эстетической точки зрения, пусть даже он и был получен не «по науке». В случае с SAM это тоже верно, поскольку данный синтезатор голоса создавался в расчете на вывод звука силами 8-битных микрокомпьютеров.
class Sam:
def __init__(self):
self.phases = [0, 0, 0, 0]
# В frame находятся параметры фрейма, voice задает тип голоса
def next(self, frame, voice):
flags, ampl1, freq1, ampl2, freq2, ampl3, freq3, pitch = frame
mix = ampl1 * math.sin(self.phases[1]) + ampl2 * math.sin(self.phases[2])
mix += ampl3 * (1 if self.phases[3] < 0.5 else -1)
self.phases[1] = (self.phases[1] + 2 * math.pi * freq1 / SR) % (2 * math.pi)
self.phases[2] = (self.phases[2] + 2 * math.pi * freq2 / SR) % (2 * math.pi)
self.phases[3] = (self.phases[3] + freq3 / SR) % 1
self.phases[0] += 1
if self.phases[0] > pitch * voice * SR:
self.phases = [0, 0, 0, 0]
return 0.5 * mix
# Коэффициенты для преобразования параметров фреймов версии SAM для С64
COEFFS = [1, 0.1, 27, 0.1, 27, 0.1, 27, 0.00001]
# Преобразование текста в массив фреймов
def parse(frames):
frames = [[int(y) * COEFFS[i] for i, y in enumerate(x.split())] for x in frames.strip().split("\n")]
return frames
# Файл sam.txt был получен с помощью программы
# https://github.com/s-macke/SAM с ключом -debug
with open("sam.txt") as f:
frames = parse(f.read())
s = Sam()
samples = []
# На каждой итерации "повышаем голос"
for voice in range(25, 5, -2):
for frame in frames:
for t in range(int(sec(0.01))):
samples.append(0.5 * s.next(frame, voice))
write_wave("sam.wav", samples)
Исходный код
Фреймы
Звучание
Ключевым в работе SAM является генератор импульсов (его индекс в массиве phases равен нулю), имитирующий работу голосовых связок, хотя он и не производит звучания сам по себе. Результатом его работы является обнуление фаз остальных осцилляторов с частотой, соответствующей выбранному типу голоса. Такой способ синтеза формант с помощью сброса фазы осциллятора-несущей осциллятором-модулятором известен в мире аналоговых синтезаторов под названием hard sync.
Пример «жесткой» синхронизации синусоиды в SAM
Изначально я хотел реализовать синтез фразы My name is Sam, но тогда шипящие и свистящие согласные пришлось бы поддержать с помощью заранее записанных звуковых фрагментов (как это и было сделано в оригинальной программе). Поэтому я остановился только на части My name. Более полным решением была бы поддержка генераторов шума с различными параметрами.
SoftSynth и TurboSynth
Одними из первых разработчиков коммерческого музыкального ПО для компьютера Apple Macintosh 128K стали Питер Готчер (Peter Gotcher) и Эван Брукс (Evan Brooks), приятели, которые играли в одной студенческой команде и увлекались как музыкой, так и программированием. В 1986 году Готчер и Брукс выпустили программу с говорящим названием SoftSynth. Вчерашние студенты реализовали полноценный аддитивный синтез со следующими параметрами:
- 32 осциллятора с выбором формы волны (синусоида, «пила», «квадрат» и 3 варианта полосового шума),
- амплитудные и частотные огибающие для управления отдельными осцилляторами, размером до 40 линейных сегментов.
В SoftSynth имелись развитые средства редактирования параметров синтеза, а также их визуализации (в том числе в виде 3d спектрограммы — в духе знаменитого синтезатора Fairlight CMI).
3d спектрограмма в SoftSynth
Редактирование огибающих осциллятора в SoftSynth
Кажется удивительным, что для реализации такого синтеза хватило возможностей древнего Macintosh. Секрет прост: процесс порождения звука в SoftSynth происходил не в реальном времени, а по нажатию на специальную иконку. Генерирование типичного короткого сэмпла занимало не более нескольких секунд. SoftSynth можно отнести к забытому ныне классу программ-генераторов сэмплов. Отвлекаясь, можно вспомнить Orangator Олега Шаронова, популярную в конце 90-х программу из этого же класса.
Интерфейс Orangator
К середине 80-х популярность аппаратных сэмплеров (таких, например, как E-MU Emulator, вышедший в 1982 году) чрезвычайно возросла, и программа, которая давала возможность синтезировать с нуля сэмплы для последующей их загрузки в память аппаратных инструментов, оказалась востребована музыкантами.
Но каким же образом в начале 80-х молодые разработчики, такие как Готчер и Брукс, получали информацию, связанную с принципами компьютерного звука? К тому времени существовали по этой тематике уже, по крайней мере, три важных источника:
- The Technology of Computer Music (1969) Макса Мэтьюса (Max Mathews), в которой был описан аудиоязык MUSIC V и простейшие методы синтеза.
- Musical Applications of Microprocessors (1980) Хэла Чемберлина (Hal Chamberlin). Эту книгу отличает практическая направленность, доступное изложение материала и достаточно широкий охват тем. Несмотря на возраст ее и сегодня вполне можно рекомендовать к прочтению тем, кто знакомится с базовыми приемами цифрового синтеза звука.
- Академический Computer Music Journal, который издается с 1977 года и где публиковались многие передовые решения, подходы, алгоритмы из области компьютерной музыки.
Очевидно, что под влиянием как модульных аналоговых синтезаторов, так и графической нотации MUSIC V из книги Мэтьюса был разработан интерфейс еще одной, весьма передовой для своего времени, музыкальной программы для компьютера Macintosh от команды Готчера и Брукса. Программа эта назвалась Turbosynth (1988). Turbosynth, как и SoftSynth, является генератором сэмплов. Пользователю предоставлена палитра звуковых модулей, экземпляры которых можно переносить мышью на рабочее пространство и, далее, соединять их по вкусу виртуальными «проводами». В программе реализовано большое количество модулей:
- осциллятор с выбором различных форм волны, возможностью морфинга этих форм с использованием шкалы времени, а также с возможностью рисования пользовательских волновых форм,
- сэмплер,
- генератор шума,
- амплитудная и частотная огибающие c произвольным добавлением сегментов с помощью мыши,
- НЧ-фильтр с огибающей,
- инвертирование спектра,
- дилей,
- различные варианты изменения высоты звука,
- AM-, FM- и PM-модуляторы,
- эффект «резонатор» на основе линий задержки,
- вэйвшейпер (waveshaper) с возможностью рисования графика функции,
- микшер.
Соединение модулей в Turbosynth
В целом работа с Turbosynth мало отличается от более поздних визуальных сред музыкального программирования (Kyma, Max/MSP, Pure Data, Reaktor). Известно, что музыкант-экспериментатор Трент Резнор (Trent Reznor, Nine Inch Nails) широко применял TurboSynth в 1993 году, во время записи альбома Mr. Self Destruct.
Как далее сложилась судьба Готчера и Брукса? Они стали известны в качестве создателей программно-аппаратного комплекса Pro Tools, который и сейчас используется во многих профессиональных студиях звукозаписи.
Практика
Сейчас я предлагаю рассмотреть еще один пример аддитивного синтеза под названием «Арпеджио Риссе». Автор этого простого, но интересного звукового алгоритма — Жан-Клод Риссе (Jean-Claude Risset), один из пионеров компьютерной музыки. В реализации «Арпеджио Риссе» используется 63 генератора синусоиды, частоты которых заданы так, чтобы эффект биений приводил к звучанию с постоянно меняющимся тембром.
def sines(bank, t):
mix = 0
for f in bank:
mix += math.sin(2 * math.pi * f * t / SR)
return mix
# Настройки параметров по Риссе
f = 96
i1 = 0.03
i2 = i1 * 2
i3 = i1 * 3
i4 = i1 * 4
risset = []
# Добавление 63 (9 * 7) частот в массив risset
for i in [f, f + i1, f + i2, f + i3, f + i4, f - i1, f - i2, f - i3, f - i4]:
for j in [i, 5 * i, 6 * i, 7 * i, 8 * i, 9 * i, 10 * i]:
risset.append(j)
samples = []
for t in range(int(sec(20))):
samples.append(0.01 * sines(risset, t))
write_wave("risset.wav", samples)
Исходный код
Звучание
«Арпеджио Риссе» является простейшим примером музыки процесса. Здесь нет деления на инструменты и ноты. Единственный алгоритм порождает все музыкальное произведение.
Следующий пример в меньшей степени связан с синтезом звука. Реализация звукового алгоритма с помощью описания блоков и связей между ними может быть представлена как в текстовом (MUSIC N), так и в графическом (TurboSynth) виде. В обоих вариантах используется подход, при котором вычисления управляются потоками данных (dataflow). Этот подход известен во многих областях информатики: теории компиляции, процессорных архитектурах, распределенных системах и т.д. Давайте попытаемся создать простую модель dataflow-вычислений в текстовой форме. Будем использовать блоки (box), которые занимаются вычислениями, а также обеспечим возможность в произвольном порядке соединять блоки проводами (wire). В таких программах, как Pure Data и Max/MSP, используется два типа связей:
- асинхронный, для, например, нотных событий,
- синхронный, для работы с аудиосигналами.
Для простоты ограничимся далее синхронными связями. Говоря конкретнее, для запуска вычисления внутри блока будем требовать соблюдения следующих условий:
- входные провода блока содержат готовые данные,
- выходные провода блока свободны для получения новых данных.
Итак, блок имеет некоторое количество входных и выходных портов. В представленной далее реализации запрещено подключение нескольких проводов к одному входному порту, но позволяется протянуть несколько проводов от единственного выходного порта блока. По этой причине в объекте блока каждый элемент массива выходных портов и сам является массивом подключенных блоков-потребителей.
# Сброс входов в незанятое состояние
def reset_ins(ins):
for i, x in enumerate(ins):
ins[i] = None
# Проверка занятости входов
def is_ins_full(ins):
for x in ins:
if x is None:
return False
return True
# Проверка готовности выходов к приему данных
def is_outs_empty(outs):
for out in outs:
for box, port in out:
if box.ins[port] is not None:
return False
return True
# Передача результатов вычисления на соотв. выходные порты
def send_to_outs(results, outs):
for i, x in enumerate(results):
for box, port in outs[i]:
box.ins[port] = x
class Box:
# Создать блок, выполняющий операцию op,
# имеющий ins входов и outs выходов
def __init__(self, op, ins, outs):
self.ins = [None] * ins
self.outs = [[] for i in range(outs)]
self.op = op
def compute(self):
if is_ins_full(self.ins) and is_outs_empty(self.outs):
send_to_outs(self.op(*self.ins), self.outs)
reset_ins(self.ins)
# Связать выходной порт port1 блока box1 с входным портом port2 блока box2
def wire(box1, port1, box2, port2):
box1.outs[port1].append((box2, port2))
def compute(schedule):
for b in schedule:
b.compute()
Для описания вычислений достаточно указать список блоков («расписание») и определить соответствующие связи. Обратите внимание на функцию compute. Она просто пытается вычислить каждый из блоков, указанный в расписании. Очередность блоков здесь не имеет значения для корректности вычислений. Однако, эта очередность влияет на эффективность вычислений. Желательно, чтобы блоки, которые вырабатывают значения, предшествовали блокам, которые эти значения потребляют. Упорядочить соответствующим образом расписание можно с помощью алгоритма топологической сортировки. Тот факт, что любой из блоков, в соответствии с правилом готовности входных и выходных данных, может независимо начать вычисления, дает возможность легко распараллелить работу графа звукового алгоритма.
Теперь давайте попробуем применить на практике реализованное выше. Займемся созданием звука сирены.
# Используется ранее определенный класс Sine
def Osc():
o = Sine()
return Box(lambda x, y: [o.next(x, y)], 2, 1)
def Out(samples):
def compute(x):
samples.append(x)
return []
return Box(compute, 1, 0)
def clip(x, y):
return -y if x < -y else y if x > y else x
Const = lambda x: Box(lambda: [x], 0, 1)
Mul = lambda: Box(lambda x, y: [x * y], 2, 1)
Clip = lambda: Box(lambda x, y: [clip(x, y)], 2, 1)
samples = []
patch = {
"k1": Const(550),
"k2": Const(50),
"k3": Const(2),
"k4": Const(0),
"k5": Const(0.1),
"k6": Const(1),
"o1": Osc(),
"o2": Osc(),
"c1": Clip(),
"m1": Mul(),
"m2": Mul(),
"out": Out(samples)
}
wire(patch["k3"], 0, patch["o1"], 0)
wire(patch["k4"], 0, patch["o1"], 1)
wire(patch["k2"], 0, patch["m1"], 0)
wire(patch["o1"], 0, patch["m1"], 1)
wire(patch["k1"], 0, patch["o2"], 0)
wire(patch["m1"], 0, patch["o2"], 1)
wire(patch["o2"], 0, patch["c1"], 0)
wire(patch["k5"], 0, patch["c1"], 1)
wire(patch["k6"], 0, patch["m2"], 0)
wire(patch["c1"], 0, patch["m2"], 1)
wire(patch["m2"], 0, patch["out"], 0)
schedule = patch.values()
while len(samples) < int(sec(2)):
compute(schedule)
write_wave("siren.wav", samples)
Исходный код
Звучание
В данном примере используются следующие типы блоков:
- Const, константа,
- Mul, умножитель,
- Osc, генератор синусоиды,
- Out, вывод сэмплов в выходной буфер,
- Clip, ограничение сигнала в пределах заданного уровня.
Графическое изображение реализации «сирены»
Может возникнуть вопрос, для чего потребовался блок Clip. С его помощью мы достигаем простейшего эффекта искажения звука. Вместо «стерильной» синусоиды получился сигнал, близкий к меандру.
Представление в графическом виде этого простого звукового алгоритма оказалось более наглядным, чем его текстовая запись. Но это, скорее, недостаток конкретной реализации. В целом, текстовая форма обычно выигрывает у графического варианта при использовании достаточно большого количества блоков и связей. В неформальном «пределе Дойча», в частности, фигурирует 50 блоков в качестве соответствующей верхней границы.
Комментарии (11)
UncleAndy
01.02.2018 10:27Спасибо за статью — довольно интересно. В свое время баловался широтно-импульсной модуляцией через «пищалку» старых компов и через АЦП для порта принтера. Во втором случае звук для тех времен был вполне пристойный.
geher
01.02.2018 11:25+1В свое время баловался широтно-импульсной модуляцией через «пищалку» старых компов
Это да. Некоторые игры выдавали на пищалку не такое уж и плохое звуковое сопровождение с музыкой.
и через АЦП для порта принтера. Во втором случае звук для тех времен был вполне пристойный.
Covox или Совокс (по сути тот же Covox, только из подручных материалов).
Помнится, паял такое.Caelwyn
01.02.2018 13:24На 386 слушал треккерную музыку через ковокс на параллельном порту, был ещё плеер, кажется VisualPlayer назывался, который мог сворачиваться в TSR и можно было что-то делать в досе, слушая музыку. Правда ресурсов мало на что оставалось.
А ещё игрался с встроенным динамиком на спектруме, по книжке в которой были примеры различных звуковых эффектов на ассемблере, иногда очень даже круто получалось.gl00my
01.02.2018 12:31+1Отличная ламповая статья! Спасибо! Как раз есть повод повозиться с генерацией звука в своем простом игровом движке. ;)
frog
03.02.2018 04:02SAM был одной из первых программ (не считая игр), которые я увидел в своё время на Commodore 64. Вот, кстати, как это выглядит и звучит:
igor_suhorukov
Отличная статья! Прямо ностальгия) Вскользь только сказали про трекерную музыку. А это был уже следующий виток по качеству на рядовом персональном компьютере без профессионального MIDI синтизатора.
true-grue Автор
Спасибо за отзыв! О трекерной и сэмплерной музыке в целом, наверное, лучше написать отдельно. Ведь тема очень обширная. Впрочем, музыкальные трекеры не всегда основывались только на сэмплах. В 90-е годы существовало несколько интересных муз. программ, которые совмещали в себе как возможности трекерной нотации, так и программный синтез. Приведу два примера.
AXS
www.youtube.com/watch?v=J8le0Zk16KY
Jeskola Buzz
www.youtube.com/watch?v=klQiIPo2w0Y
Сегодня же в данной категории одним из лидеров является отечественный SunVox
www.youtube.com/watch?v=J7U32h7Va1c