REPL 🌀 Bulka

REPL

Хотя Strudel можно использовать как библиотеку в любой кодовой базе JavaScript, его основным справочным пользовательским интерфейсом является Strudel REPL1, который является браузерной средой live coding. Этот редактор live code предназначен для манипулирования Strudel patterns во время их воспроизведения. REPL имеет встроенную визуальную обратную связь, подсвечивая, какие элементы в pattern (mini-notation) последовательностях влияют на событие, которое в данный момент воспроизводится. Эта обратная связь разработана для поддержки как обучения, так и живого использования Strudel.

Помимо UI для управления воспроизведением и мета-информации, основной частью интерфейса REPL является редактор кода на базе CodeMirror. В нем пользователь может редактировать и оценивать код pattern вживую, используя один из доступных выходов синтеза для создания музыки и/или звукового искусства. Поток управления REPL следует 3 основным шагам:

  1. Пользователь пишет и обновляет код. Каждое обновление транспилирует и оценивает его для создания экземпляра Pattern
  2. Пока REPL работает, Scheduler запрашивает активный Pattern с регулярным интервалом, генерируя Events (также известные как Haps в Strudel) для следующего временного диапазона.
  3. Для каждого тика планирования все сгенерированные Events запускаются путем вызова их метода onTrigger, который устанавливается выходом.

Пользовательский код

Чтобы создать Pattern из пользовательского кода, необходимы два шага:

  1. Транспилировать входной JS код, чтобы сделать его функциональным
  2. Оценить транспилированный код

Транспиляция и оценка

В мире JavaScript использование транспиляции является обычной практикой, чтобы иметь возможность использовать языковые функции, которые не поддерживаются базовым языком. Такие инструменты, как babel, будут транспилировать код, содержащий неподдерживаемые языковые функции, в версию кода без этих функций.

В той же традиции Strudel может добавить шаг транспиляции для упрощения пользовательского кода в контексте live coding. Например, Strudel REPL позволяет пользователю создавать mini-notation patterns, используя только строки в двойных кавычках, в то время как строки в одинарных кавычках остаются тем, чем они являются:

note("c3 [e3 g3]*2")

транспилируется в:

note(m('c3 [e3 g3]', 5))

Здесь строка обернута в m, который создаст pattern из строки mini-notation. В качестве второго параметра передается расположение исходного кода строки, что позволяет подсвечивать активные события позже.

После транспиляции код готов к оценке в Pattern.

За кулисами строка пользовательского кода парсится с помощью acorn, превращаясь в абстрактное синтаксическое дерево (AST). AST позволяет изменять структуру кода перед генерацией транспилированной версии с помощью escodegen.

Mini-notation

Хотя транспиляция позволяет JavaScript выражать Patterns менее многословным способом, все же предпочтительнее использовать mini-notation как более компактный способ выражения ритма. Strudel стремится предоставить те же функции и синтаксис mini-notation, что и в Tidal.

Парсер mini-notation реализован с помощью peggy, который позволяет генерировать производительные парсеры для доменных языков (DSL) с использованием краткой грамматической нотации. Сгенерированный парсер превращает строку mini-notation в AST, который используется для вызова соответствующих функций Strudel с заданной структурой. Например, "c3 [e3 g3]*2" приведет к следующим вызовам:

seq(
  reify('c3').withLoc(6, 9),
  seq(reify('e3').withLoc(10, 12), reify('g3',).withLoc(13, 15))
)

Подсветка расположений

Как видно в примерах выше, как транспилятор, так и парсер mini-notation добавляет расположение исходного кода, используя withLoc. Это расположение вычисляется внутри функции m как сумма 2 расположений:

  1. расположение, где начинается строка mini notation, полученное из JS парсера
  2. расположение подстроки внутри mini notation, полученное из парсера mini notation

Сумма обоих передается в withLoc, чтобы сообщить каждому элементу его расположение, которое может быть позже использовано для подсветки, когда он активен.

Mini Notation

Другая важная часть пользовательского кода - это mini notation, которая позволяет выражать ритмы кратким образом.

  • mini notation реализована как PEG грамматика, находящаяся в mini package
  • она основана на krill от Mdashdotdashn
  • peg грамматика используется для генерации парсера с помощью peggyjs
  • сгенерированный парсер берет строку mini notation и выдает AST
  • AST затем может быть использовано для построения pattern с использованием обычного Strudel API

Вот пример AST для c3 [e3 g3]

{
  "type_": "pattern",
  "arguments_": { "alignment": "h" },
  "source_": [
    {
      "type_": "element", "source_": "c3",
      "location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } }
    },
    {
      "type_": "element",
      "location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } }
      "source_": {
        "type_": "pattern", "arguments_": { "alignment": "h" },
        "source_": [
          {
            "type_": "element", "source_": "e3",
            "location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } }
          },
          {
            "type_": "element", "source_": "g3",
            "location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } }
          }
        ]
      },
    }
  ]
}

который переводится в seq(c3, seq(e3, g3))

Vim горячие клавиши

См. отдельную страницу о горячих клавишах Vim для быстрого справочника: /technical-manual/vim

Планирование событий

После получения экземпляра Pattern из пользовательского кода, он используется планировщиком для запроса событий. После запуска планировщик выполняется с фиксированным интервалом для запроса активного pattern на предмет событий в пределах временного диапазона текущего интервала. Упрощенная реализация выглядит так:

let pattern = seq('c3', ['e3', 'g3']); // pattern от пользователя
let interval = 0.5; // интервал запроса в секундах
let time = 0; // начало текущего временного диапазона
let minLatency = 0.1; // минимальное время до того, как hap должен сработать
setInterval(() => {
  const haps = pattern.queryArc(time, time + interval);
  time += interval; // увеличить время
  haps.forEach((hap) => {
    const deadline = hap.whole.begin - time + minLatency;
    onTrigger(hap, deadline, duration);
  });
}, interval * 1000); // запрос каждые "interval" секунд

Обратите внимание, что приведенный выше код упрощен в иллюстративных целях. Фактическая реализация должна обходить неточные обратные вызовы setInterval. Подробнее о деталях реализации можно прочитать в этом блог-посте.

Тот факт, что Pattern.queryArc является чистой функцией, которая отображает временной диапазон на набор событий, позволяет нам выбрать любой интервал, который нам нравится, без изменения результирующего вывода. Это также означает, что когда pattern изменяется извне, следующий обратный вызов планирования будет работать с новым pattern, сохраняя его часы запущенными.

Задержка между временем оценки pattern и временем, когда изменение слышно, составляет от minLatency до interval + minLatency, в нашем примере от 100 мс до 600 мс. В Strudel текущий интервал запроса составляет 50 мс с minLatency 100 мс, что означает, что задержка составляет от 50 мс до 150 мс.

Выход

Последний шаг - запустить каждое событие в выбранном выходе. Здесь заданное время и значение каждого события используются для генерации аудио или любой другой формы временного вывода. Выходом по умолчанию Strudel REPL является вывод WebAudio. Чтобы понять, что делает выход, мы сначала должны понять, что такое контрольные параметры.

Контрольные параметры

Чтобы иметь возможность манипулировать несколькими аспектами звука параллельно, так называемые контрольные параметры используются для формирования значения каждого события. Пример:

note('c3 e3')
  .cutoff(1000)
  .s('sawtooth')
  .queryArc(0, 1)
  .map((hap) => hap.value);
/* [
  { note: 'c3', cutoff: 1000, s: 'sawtooth' }
  { note: 'e3', cutoff: 1000, s: 'sawtooth' }
] */

Здесь используются функции контрольных параметров note, cutoff и s, где каждая контролирует другое свойство в объекте значения. Каждая функция контрольного параметра принимает примитивное значение, список значений для секвенирования в Pattern или Pattern. В примере note получает Pattern из выражения mini-notation (в двойных кавычках), в то время как cutoff и s получают Number и (в одинарных кавычках) String соответственно.

Strudel поставляется с большим набором функций контрольных параметров по умолчанию, которые основаны на используемых в Tidal и SuperDirt, фокусируясь на музыкальной и аудио терминологии. Однако можно создавать пользовательские контрольные параметры для любых целей:

const { x, y } = createParams('x', 'y');
x(sine.range(0, 200)).y(cosine.range(0, 200));

Этот пример создает пользовательские контрольные параметры x и y, которые затем используются для формирования pattern, описывающего координаты круга.

Выходы

Теперь, когда мы знаем, как значение события манипулируется с помощью контрольных параметров, мы можем посмотреть, как выходы могут использовать это значение для генерации чего угодно. Планировщик выше вызывал функцию onTrigger, которая используется для реализации вывода. Очень простая версия вывода web audio может выглядеть так:

function onTrigger(hap, deadline, duration) {
  const { note } = hap.value;
  const time = getAudioContext().currentTime + deadline;
  const o = getAudioContext().createOscillator();
  o.frequency.value = getFreq(note);
  o.start(time);
  o.stop(time + event.duration);
  o.connect(getAudioContext().destination);
}

Приведенный выше пример создаст OscillatorNode для каждого события, где частота контролируется параметром note. По сути, так работает вывод WebAudio API в Strudel, только с гораздо большим количеством параметров для управления синтезаторами, сэмплами и эффектами.

Я хочу помочь, как мне внести вклад в документацию?

Footnotes

  1. REPL расшифровывается как read, evaluate, print/play, loop (читать, оценивать, печатать/воспроизводить, цикл). Это дружественный жаргон для интерактивного программного интерфейса из наследия вычислений, обычно для интерфейса командной строки, но также применяется к редакторам live coding.