Pattr

Гранулярный Синтез в PureData

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

Создание гранулы

Начнём с самого простого, с гранулы. Для начала создадим table (назовём его sample, например, [table sample]), в который у нас загружен какой-нибудь семпл. Но нам не нужно его проигрывать сначала до конца, а нужен кусок, гранула. Для этой цели подходит объект tabread~, он считывает значения из table. Например, если мы на вход tabread~ подадим число 200, объект выдаст значение двухсотого сэмпла. Получается, для того, чтобы получить на выходе звук, нам надо как-то последовательно считатывать сэмплы, но как? Эту задачу можно решить с помощью объекта phasor~. Напомню, что этот объект генерирует пилообразный сигнал со значениями от 0 до 1. Но если мы просто подсоединим этот объект к tabread~, получится, что будут читаться только два сэмла. Чтобы прочитать, скажем, первые 200 сэмплов, нужно просто умножить выход phasor~ на 200. После данной операции значение этот сигнал будет изменяться уже от 0 до 200. Все было бы хорошо, но задавать все эти значения в сэмплах как-то неудобно, намного привычнее работать с миллисекундами, значит, надо преобразовать миллисекунды в сэмплы. Это делается очень легко: надо всего лишь умножить значение в миллисекундах на 44.1 (это частота дискретизации, разделенная на 1000, соответственно, при работе с другой частотой дескритизации, надо изменить это значение).  Получается 50 миллисекундная гранула это 44.1 * 50  = 2205 семплов.

Теперь надо определить частоту объекта phasor~, так как если она будет постоянной, скажем, 1 Гц, то наш фазор за одно и то же время будет считывать гранулы разных размеров. Для расчета частоты надо разделить 1000 (миллисекунд) на размер гранулы. Иными словами если размер гранулы 50 мс, частота phasor~ будет  1000ms / 50ms = 20Hz. 50 мс — это отрезок времени, за который phasor~ сделает одно колебание.

Если включить аудио , и поиграть с намбер боксом grain size, мы услышим эту самую гранулу, то есть зацикленный фрагмент семпла будет проигрыватся с начала до той миллисекунды которая указана в grain size.

Перемещение гранулы по таблице

Итак, что такое гранула разобрались. Теперь нужно заставить ее перемещаться по семлу. Для этого, мы должны прибавить к нашму пилообразому сигналу значение. Допустим нам нужно проигрывать кусок семпла с 120-й по 140-ю миллисекунду. Для этого создаётся ещё один намбер бокс (назовём его grain position), выход с которого умножается опять на 44.1, и данный сигнал складывается с гранулой, после чего подаётся в [tabread~ sample]. Значение grain position в данном случае будет 120, а grain size 20, так как 120 + 20 = 140. Тут одна загвоздка – треск, возникающий при изменении позиции гранулы. Его можно устранить, если задерать значение с grain position до того времени как стартует гранула. Сделать это можно с помощью объекта samphold~  который выводит значение, поступившее в его левый вход, каждый раз, когда сигнал в правом входе уменьшается. В нашем случае в левый вход будет подаватся  позиция гранулы, а в правый выход с phasor~. Получается, что объект будет выводить значение только, когда phasor~ завершит цикл и совершит переход от 1 к 0.

Однако, треск всё ещё остался, но уже по другой причине – он возникает при переходе с конца гранулы в начало. Чтобы уменьшенить треск, нужно создать огибающую амплитуды. Для этого существует довольно распространённый “рецепт”: сигнал с phasor~ a проходит ряд пробразований  (-~ 0.5 => *~ 0.5 => cos~), после которого получаем синхронизированную с гранулой огибающую, представляющую собой половину косинусовой волны. Теперь остается перемножить выход с tabplay~ и созданную огибающую.

Итак, мы создали одну гранулу. Но гранулярный синтез по-настоящему проявляет себя, когда одновременно играют несколько гранул.

Что ж, теперь нам надо добавить еще одну гранулу, позиция которой будет немного смещена относительно первой гранулы. Тут нам понадобится объект wrap~. Он на выходе дает разность поступающего в него числа и его целой части (например, если на вход подать 5.22, на выходе будет 0.22). Но как же это он может помочь сместить фазу, скажем, на 0.5? А вот как. Сначала мы добавим к выходу phasor~ 0.5, это даст нам на выходе пилу, со значениями от 0.5 до 1.5. Затем этот сигнал мы подаем в объект wrap~. Который снова нам дает пилу с диапазоном 0…1. Только вот что происходит, когда на phasor~ выдает значение 0, wrap~ преобразует его в 0,5; когда phasor~ - 0,5, wrap~ - 0,5 и т.д.

Теперь остается лишь скопировать большую часть первой гранулы (все, кроме того, что относится к объекту phasor~. Вот, что получилось:

До сих пор мы могли перемещать гранулу по семплу только вручную. Но это абсолютно не пригодно для музыкальных целей. У нас есть 2 пути решения этой проблемы:

  1. каждый раз гранула будет воспроизводиться со случайного места;
  2. за перемещение гранулы будет отвечать какая-либо кривая.

Начнем по-порядку. В первом случае за положение гранулы будет отвечать объект noise~, генерирующий белый шум. По сути, белый шум представляет собой набор случайных чисел от -1 до 1, генерирующихся, в нашем случае, 44100 раз в секунду (опять же, все зависит частоты дескритизации проекта). Идея заключается в том, чтобы  при каждом завершении цикла гранулы, noise~ давал нам новое значение параметра grain_position.

Итак, приступим к действию. Для начала, выход из noise~ мы  подадим в объект abs~, который делает отрицательные числа положительными (не может же быть номер сэмпла отрицательным, правда?).  Затем помножим полученное на значение, взятое из объекта soundfiler, который каждый раз при загрузке аудиофайла выдает количество сэмплов, содержащихся в нем. Так, далее. noise~ генерирует числа 44100 раз в секунду, но нам нужны значения лишь только, когда гранула завершает свой цикл. Для этого нам нужен знакомый объект samphold~. На его левый вход подаем преобразованный noise~, а на правый выход из объекта phasor~.

Перейдем ко второму методу. Табличному. С его помощью можно указывать путь перемещения гранулы, а также скорость.

Создаём ещё одну таблицу меню->put->array. Заходим в свойства таблицы и меняем y range from 1 -1 на 1 0. Ставим галочку на bezier curve  (так кривая выглядит плавнее). Назовём таблицу grain_pos. Теперь нам нужно задать размер таблицы , для этого создаём сообщение [;grain_pos resize $1]  и вводим в него выход с soundfiler. Теперь, когда мы загружаем файл в таблицу [table sample], размер  таблицы grain_pos будет равен размеру [table sample]. Далее создаём ещё один phasor~, который будет управлять скоростью перемещения гранулы и присоединяем к нему слайдер, который будет управлять частотой его (в свойствах слайдера зададим границы, скажем, 0.1 и 4). При значении 0.1 одно полное колебание phasor~  составит 1/0.1 = 10 секунд, это означает что семпл сначала до конца (а точнее, как мы пропишем в grain_pos) будет проигрыватся 10 секунд, при  частоте 4Hz 1/4 = 0.25 секунды (или 250 мс). Теперь нам надо умножить сигнал из phasor~ на количество семплов, после чего направляем сигнал в  [tabread~ grain_pos]. Выход из этого объекта надо вновь  умножить на количество семплов (так как Y range в таблице у нас был 1..0). Ну и естественно, всё что получилось направляем в объекты samphold~  двух наших гранул. Все, можно мышкой рисовать кривые в таблице grain_pos и радоваться :).

Заключение

Так, вроде бы синтезатор готов, но в нем все еще есть, что доработать. Вот список вещей, которые требуют доработки, пусть это будет домашним заданием.

  1. Количество гранул. Две гранулы – это не круто. Больше гранул – «плавнее» звук. Когда будете их добавлять, обратите внимание на смещение фаз. Если гранулы три, то оно будет 0.33, если две – то фаза будет смещаться уже на 0.25. Хотя, можно вообще сделать, чтобы каждая гранула играла разные куски сэмпла, этого никто не запрещает ;)
  2. Версия патча со случайным перемещением гранул имеет недостаток: объект noise~ может выбрать начало гранулы на предпоследнем сэмпле, что вызовет искажения. Это решается простым вычитанием из размера аудиофайла (в сэмплах) размера гранулы (тоже в сэмплах);
  3. Патч все еще не может контролировать питч гранул. Для того, чтобы сделать это надо умножить значение, которое задает частоту phasor~ на какое-либо число.

Надеемся, эта статья помогла разобраться с принципом работы гранулярного синтеза. С PureData или Max/MSP ваши возможности безграничны. Можно создать хоть 100 гранул, каждая со своим питчем, позицией, можно различные гранулы посылать на разные дилей линии, главное, чтобы хватило ресурсов компьютера.

16 Jul 2011  Bruce Lee, OSCII