Настройка
Я собираюсь представить инструмент для синтеза звука и программирования музыки под названием Supercollider. Он содержит как аудиосервер, так и язык программирования под названием sclang. Аудиосервер генерирует звук и может быть использован с любого языка программирования, используя протокол OSC. Sclang — это язык, похожий на ruby, который является чисто объектно-ориентированным и ведет себя немного похоже на smalltalk. Вы посылаете сообщения объектам, которые затем могут выполнять различные блоки кода или передавать параметры. Он должен быть достаточно знакомым, если вы знаете языки типа C, ruby или python, но я также расскажу о синтаксисе. Вы должны, по крайней мере, знать, что такое функции, объекты, переменные и аргументы. Также будет полезно, если у вас есть базовое понимание математики, например, синусоиды, и некоторые знания музыки.
Сначала загрузите Supercollider с веб-сайта. Используйте программу установки для Mac и Windows, для linux есть tarball исходников или репо-пакеты, которые можно использовать. Затем запустите supercollider и вы должны увидеть окно кода с консольным окном справа и документацией. Окно документации содержит всю документацию, которую вы можете просматривать, а окно консоли содержит статус сервера и стандартный вывод.
Поскольку Supercollider использует архитектуру клиент-сервер, вы должны загрузить сервер, который использует sclang. Нажмите на Server->Boot Server в меню сверху. Поскольку я работаю на Mac, я собираюсь использовать комбинацию клавиш Cmd-B, которая также запускает сервер. Вы также можете использовать сообщение s.boot;
, которое загрузит сервер по умолчанию, определенный в s.
Вы должны увидеть, что элементы в нижней части окна консоли стали зелеными. Это означает, что сервер загрузился, а также некоторые сообщения. Вы можете получить сообщение об ошибке, возможно, связанной со скоростью звука, найдите все полученные ошибки и исправьте их.
Переменные и функции
Сначала я собираюсь представить некоторые особенности языка. Поскольку Supercollider почти полностью ориентирован на объекты, он ведет себя немного как Ruby, где вы можете посылать сообщения объектам. Например, я могу набрать "Hello Supercollider".postln;
. Чтобы запустить его, выберите блок кода и используйте комбинацию клавиш Shift-Enter. Вы также можете выбрать в меню пункт Язык->Оценить выделение, линию или область.
Вы должны увидеть текст Hello Supercollider в окне консоли в правом нижнем углу. В этом примере мы отправили сообщение postln на строку «Hello Supercollider». Высказывания заканчиваются точкой с запятой, но если у нас только одно высказывание, мы можем их опустить. Мы также можем выполнять арифметические действия, как и в других языках на базе C, например. ((4+5)/2).postln;
.
Мы должны получить ответ 4.5
Есть и более интересные сообщения, которые можно применить к числам. Например, нахождение первых 5 чисел Фибоначчи 5.fib
или нахождение квадратного корня 5.sqrt
. В Supercollider мы имеем те же стандартные типы данных: строки, числа, массивы (списки), а также новые типы данных, такие как сигналы, которые мы рассмотрим позже.
Переменные являются краеугольным камнем любого вида процедурного программирования, и в Supercollider у нас есть два типа переменных. Глобальные и локальные переменные. Глобальные переменные определяются во всем файле в любой области видимости. Если имя переменной состоит всего из одной буквы, вы можете просто использовать ее напрямую, например, так a = 3
.
Для более длинных имен глобальных переменных вы можете использовать префикс ~, например, так ~myGlobal = 2
.
Локальные переменные определяются с помощью ключевого слова var и существуют в определенной области видимости, определяемой блоками кода или функцией. Блоки кода — это масштабируемые блоки кода, которые выполняются все сразу. Для этого мы заключаем код в круглые скобки, как показано ниже.
(
var x = 2;
y = 3
)
(
x.postln;
y.postln;
)
Когда вы щелкаете на первой скобке и делаете Shift-Enter, блок запускается и присваивает переменные. Второй блок кода при выполнении дает нам nil и 3. Nil — это нулевое значение, и оно возникает потому, что мы находимся вне области видимости x, однако поскольку y — глобальная переменная, она может существовать в любой области видимости.
Мы можем использовать блоки кода для одновременного выполнения кода, но мы также можем захотеть дать имя нашим блокам и передать аргументы. Для этого мы используем функции, определенные фигурными скобками, как в C. Однако функции работают немного по-другому, поскольку они не типизированы.
(
var add = { arg x, y; x + y };
add.value(1, 2);
)
Функция add определяется с помощью фигурных скобок и списка аргументов после ключевого слова arg. Последняя строка в функции — это то, что возвращается.
Чтобы найти значение функции, выполняемой с аргументами, можно воспользоваться сообщением value. Функции также являются функциями высшего порядка, что означает, что мы можем передавать их в качестве аргументов.
(
var applyTwice = { arg f, x; f.value(f.value(x)) };
applyTwice.value({arg x; 2 * x}, 1);
)
Здесь можно использовать много интересных приемов функционального программирования, но это слишком сложно для данного учебника.
Аргументы функции могут быть определены несколькими способами. Мы можем использовать ключевое слово arg вот так.
Или с помощью оператора || и списка аргументов внутри.
Большинство алгоритмов в sclang реализованы с помощью функций и техник функционального программирования в сочетании с объектно-ориентированным программированием. Традиционные процедурные техники, такие как циклы while, существуют, но используются не так часто.
Циклы и условия
Теперь мы переходим к циклам, которые обычно выполняются немного иначе, чем во многих языках типа C. При выполнении циклов в supercollider мы обычно используем циклы на основе сообщений, как в Javascript или Ruby. Например, я могу использовать сообщение do, чтобы выполнить цикл несколько раз.
(
5.do {
arg n;
(n*n).postln;
}
)
В 5 передается сообщение do, которое выполняет функцию пять раз. В качестве аргумента передается текущая итерация, которая затем возводится в квадрат.
Мы также можем выполнять условия, как операторы if. Хотя немного иначе, чем в других языках. Мы используем if как вызов функции с условием, пунктом then и, наконец, пунктом else, передаваемыми в качестве функций. Поскольку условие не меняется, мы не передаем его как функцию.
(
var x = 4;
if(x < 5, {"X is less than 5".postln}, {"X is greater than or equal to 5".postln});
)
Мы также можем использовать циклы while, которые выглядят аналогично, но поскольку условие меняется, нам нужно использовать функцию.
(
var x = 0;
while({x < 10}, {x = x + 1; x.postln});
)
Мы также можем делать циклы for, как в Python или Ruby.
for(1, 10, { |x| x.postln})
Массивы
Массивы — это последовательный тип данных в суперколлайдере. Они ведут себя примерно так же, как списки в Python.
Вы можете перебирать элементы массива.
[1,2,3,4].do { |x| x.postln; }
Мы можем создать новый массив и действительно любой новый объект с новым сообщением.
(
var a = Array.new;
a = a.add(5);
a = a.add(6);
a.postln;
)
Массивы должны переназначаться каждый раз, когда добавляется новый элемент. Мы можем получить размер с помощью сообщения size.
(
var a = [1,2,3,4];
a.size.postln;
)
Мы можем получить определенный элемент из массива с помощью функции at.
[1,2,3,4].at(0);
Мы можем использовать сообщение fill для заполнения массива функцией определенного размера.
(
var a = Array.fill(5, { |x| 2**x });
a.postln;
)
Осцилляторы
Основой Supercollider является синтез звука. По сути, генерация звуков, которые в основном являются сигналами, изменяющимися во времени. Вы можете представить себе это как массив поплавков, обычно два массива, потому что аудиовыход имеет два канала. Высота сигнала — это амплитуда, а скорость, с которой он движется, называется частотой. Более высокая частота создает более высокий звук, а более низкая частота — более низкий звук. Самая низкая частота, которая обычно имеет наибольшую амплитуду, называется основной частотой, а все последующие частоты называются парциальными.
Звук генерируется с помощью осцилляторов, которые позволяют создавать динамические сигналы, изменяющиеся во времени. Мы можем воспроизвести их, используя сообщение play для функции, подобной этой.
{ SinOsc.ar(440) }.play;
Воспроизводится синусоидальная волна с частотой 440 Гц. Чтобы остановить звук, можно использовать комбинацию клавиш Cmd — . на mac. Есть и другие параметры, которыми мы можем манипулировать, такие как фаза и mul — коэффициент амплитуды. Я рекомендую посмотреть видео по обработке сигналов, где они объясняют частоту, фазу и т.д., чтобы получить представление об этом.
Мы можем построить график сигнала с помощью сообщения plot.
{ SinOsc.ar(440, ) }.plot;
Или просмотреть осциллограмму во время воспроизведения звука.
{ SinOsc.ar(440, ) }.scope;
Mul ослабляет сигнал, умножая его на коэффициент, а add прибавляет к сигналу.
{ SinOsc.ar(freq: 440, mul: 0.5, add: 0.1) }.scope;
Мы можем думать о сигнале как о модифицированном таким образом.
Мы можем выводить несколько сигналов на разные каналы, передавая массив.
{ SinOsc.ar([440,660]) }.scope;
Есть и другие осцилляторы, которые мы можем использовать, например, Saw и Pulse. Я бы изучил их и посмотрел, чем они отличаются от SinOsc. Осцилляторы — это тип объектов под названием Unit Generators, которые составляют основу синтеза звука в суперколлайдере. Существует множество UGens, которые могут производить звук, фильтровать звук и т.д. Для примера мы используем фильтр, чтобы отфильтровать резкие звуки пильного осциллятора.
(
{
var sig = Saw.ar(440);
LPF.ar(sig, freq: 500);
}.scope;
)
Возможно, вы заметили, что я использую сообщение ar на UGens. Это означает скорость звука и используется для сигналов, которые представляют выходной звук, мы также можем использовать сообщение kr (скорость управления) для сигналов, которые управляют другими сигналами, например, для управления частотой сигнала.
(
{
var control = SinOsc.kr(2).range(220, 440);
var sig = Saw.ar(control);
LPF.ar(sig, freq: 500);
}.scope;
)
Это создает управляющий сигнал, работающий два раза в секунду, который проходит между 220 и 440 и управляет частотой сигнала нашей пилы.
Мы можем использовать некоторые интересные UGens, такие как Mix, для объединения сигналов. Сообщение fill ведет себя примерно так же, как и для массива, за исключением того, что оно складывает все сигналы вместе, создавая один сигнал с фундаментальным на 220 и 4 другими парциальными после него. Мы можем просмотреть это с помощью диапазона частот, выполнив команду FreqScope.new
или просто перейдя в меню Server->Show FreqScope. Это отобразит частоты, составляющие текущий сигнал. Попробуйте запустить этот код и посмотрите, что произойдет.
(
FreqScope.new;
{
Mix.fill(5, { |x| SinOsc.ar(220*x) })
}.scope;
)
Мы можем создавать более интересные звуки, увеличивая количество партиций или изменяя волну.
Сигналы можно складывать, вычитать, умножать — все, что мы можем делать с числами, мы можем делать и с сигналами.
(
{
var fund = 220;
SinOsc.ar(fund) + SinOsc.ar(fund*2) + SinOsc.ar(fund*4);
}.scope;
)
Если посмотреть на фрикскоп, можно увидеть, что теперь у нас есть три частоты с частицами на 440 и 880. Любой звук может быть составлен из серии синусоидальных частот, и все, что нам нужно сделать, это сложить их, чтобы получить звук. Это называется аддитивным синтезом. Субтрактивный синтез — это когда мы отнимаем от звука что-то, например, используем фильтры, чтобы сделать из него то, что нам нужно, как здесь, где мы используем фильтр низких частот, чтобы пропускать только частоты ниже определенного диапазона. Есть также фильтр высоких частот, который пропускает только более высокие сигналы.
(
{
var fund = 440, cutoff=600;
LPF.ar(Pulse.ar(fund), freq: cutoff);
}.scope;
)
Существуют и другие типы синтеза, но сейчас мы сосредоточимся на этих конкретных типах.
SynthDefs
Иногда мы хотим сохранить наши сигналы и иметь возможность использовать их позже. Например, мы можем использовать синтезатор для воспроизведения различных нот. Мы можем хранить наши сигналы в SynthDefs, которые являются объектами, способными создавать и изменять звук. Для создания SynthDef мы передаем символ, представляющий имя, и функцию с аргументами.
(
SynthDef(saw, {
arg freq=440, cutoff=1000;
var sig;
sig = Saw.ar(freq);
sig = LPF.ar(sig, cutoff);
Out.ar(0, sig);
}).add;
)
Synth(saw, [freq, 440, cutoff, 300]);
Мы можем создать синт, а затем передать аргументы при запуске. Сообщение add вызывается на SynthDef, чтобы добавить его на сервер. Out UGen используется для отправки сигнала на стереовыход, пока что используйте шину номер 0. Символы начинаются с и используются для названия синтов, кроме всего прочего, пока что используйте простые имена.
Позже мы сможем задать аргументы для синтезатора с помощью сообщения set.
(
var asynth = Synth(saw);
asynth.set(freq, 550);
)
Конверты
Это замечательно, за исключением того, что наш звук, кажется, длится вечно. Мы можем сделать так, чтобы он длился только определенное время, используя огибающие. Огибающие могут влиять на амплитуду, частоту или любое другое свойство сигнала, но для уменьшения громкости сигнала мы будем влиять на амплитуду.
(
Env([1,0], [1]).plot;
SynthDef(saw, {
arg freq=440, cutoff=1000;
var sig, env;
env = EnvGen.kr(Env([1, 0], [1]));
sig = Saw.ar(freq);
sig = LPF.ar(sig, cutoff) * env;
Out.ar(0, sig);
}).add;
Synth(saw);
)
Мы передаем Env в EnvGen UGen, который генерирует огибающую и использует ее для управления амплитудой сигнала. Параметрами Env являются уровни, которые могут изменяться от 0 до 1, и время, которое представляет собой количество времени в секундах. Время будет на единицу меньше, чем количество. Мы видим, как сигнал линейно уменьшается с течением времени. Мы можем создать огибающую, которая будет увеличиваться, а затем уменьшаться, как показано ниже.
(
Env([0, 1, 0], [1, 1]).plot;
SynthDef(saw, {
arg freq=440, cutoff=1000;
var sig, env;
env = EnvGen.kr(Env([0, 1, 0], [1, 1]));
sig = Saw.ar(freq);
sig = LPF.ar(sig, cutoff) * env;
Out.ar(0, sig);
}).add;
Synth(saw);
)
Вы заметили, как сигнал затухает и исчезает. Это происходит потому, что мы начинаем с 0 и доходим до 1 за одну секунду, а затем возвращаемся к 0 за другую секунду. Таким образом, для завершения звука требуется 2 секунды. Существуют и другие способы манипулирования огибающей, например, изменение кривой. Существуют также другие типы огибающих, например perc и asdr. Я не буду сейчас углубляться в эти темы, просто поищите их и попробуйте использовать.
Паттерны
Теперь мы переходим к секвенции звуков. Это то, что позволяет нам играть музыку. В Supercollider это можно сделать с помощью паттернов. Паттерны — это как массивы, которыми можно манипулировать, воспроизводить и использовать бесконечно. Основная функция, которую мы используем для создания паттернов, — Pbind. Мы используем Pbind с модификаторами, такими как Pseq, для создания сложных паттернов.
Pbind(freq, 440).play;
Это запускает повторяющуюся ноту с частотой 440 Гц.
Мы можем бесконечно воспроизводить серию восьмых нот с последовательностью возрастающих нот, начиная со средней C, со случайной амплитудой.
(
Pbind(
dur, 0.125,
midinote, Pseq([0, 1, 2, 3, 4] + 60, inf),
amp, Prand([0.125, 0.5, 0.25], inf)
).play;
)
Мы можем изменить инструмент на синтезатор кулачковой пилы, который мы определили ранее. Запустите этот код снова, а затем запустите этот код, чтобы придать звуку более богатый тембр.
(
Pbind(
dur, 0.25,
midinote, Pseq([0, 1, 2, 3, 4] + 60, inf),
amp, Prand([0.125, 0.5, 0.25], inf),
instrument, saw
).play;
)
Мы можем выбрать серию случайных нот до мажора, создав случайную мелодию.
(
Pbind(
dur, 0.25,
midinote, Prand([0, 2, 4, 5, 7, 9] + 60, inf),
amp, Prand([0.125, 0.5, 0.25], inf),
instrument, saw
).play;
)
Возможно, я напишу целый пост о паттернах, но пока этого должно быть достаточно. Просто поиграйте с различными паттернами и посмотрите на различные типы генераторов паттернов.
Надеюсь, вам понравилась эта статья, просто поиграйте с примерами и попробуйте создать более сложные SynthDefs. Меняйте параметры и манипулируйте их взаимодействием.