Слишком часто я наблюдал за тем, как импровизирующий музыкант трясущимися руками пытается увеличить pdf размером A4 на крошечном экране телефона в самом разгаре исполнения. Мы обязаны создать плавный и отзывчивый рендеринг музыки для веба!
В вебе нотная запись должна быть столь же доступной и плавной, как текст; однако пока это не так, и это уязвляет мои чувства. Давайте решим эту актуальную проблему.
SVG, отрендеренный Scribe 0.2
Несколько лет назад я создал прототип рендерера музыки, который назвал Scribe. Он выполняет преобразование JSON в SVG. Изначально я стремился к созданию адаптивного рендерера музыки. Это было хорошее демо, но для дальнейшего развития пришлось бы писать сложный многопроходный движок генерации макетов, а у меня тогда возникли другие дела.
Вскоре после этого я занялся адаптированием Grid под проекты компании, и тут мне почудилось нечто знакомое: я задался вопросом, а не станет ли он решением некоторых проблем, с которыми я столкнулся при разработке Scribe?
Нотный стан выстроен в подобие сетки. Высота ноты откладывается по вертикальной оси, а время идёт влево по горизонтальной оси. Я определю эти две оси в двух отдельных классах. Вертикальная ось, описывающая строки сетки, будет называться
Если применить этот код к
Неплохо. Не особо информативно, но, изучив результат, мы увидим, что у каждой линейки и у каждого пустого поля теперь есть собственная строка сетки с названием высоты ноты, обозначающая каждую строку:
Каждая из строк стана может содержать одну из нескольких высот нот. Например, ноты G♭, G и G♯ должны находиться на одной линейке G.
Чтобы разместить описывающие эти ноты элементы DOM в нужные строки, я помещу названия нот в атрибуты
Это правило обрабатывает ноты, начинающиеся с
И этого будет достаточно, чтобы начать размещать символы на нотном стане! У меня есть SVG-символы, которые я подготовил для прототипа Scribe. Давайте попробуем поместить парочку на стан:
Выглядит многообещающе. Теперь займёмся временем.
С ритмом, пожалуй, работать сложнее. Не сразу понятно, какую выбрать наименьшую часть ритма, поддерживающую все возможные ритмы. Необходимо принять решение о том, какие минимальные длительности нот и какие кросс-ритмы нужно поддерживать в рамках сетки.
Если разделить такт на 24 столбца, то мы можем равномерно распределять восьмые (12 столбцов), шестнадцатые (6 столбцов), 32-е (3 столбца), а также значения триолей этих нот. Неплохо для начала.
Вот четырёхдольный такт, определённый как 4 × 24 = 96 столбцов сетки, плюс по столбцу в начале и в конце:
Добавим пару тактовых черт как контент
При внимательном изучении можно заметить, что ключ попал в первый столбец, и что есть 96 столбцов нулевой длины, по 24 на долю, каждый из которых разделён небольшим
Теперь я воспользуюсь атрибутами
Селектор атрибута
Соединив это с классом
Отлично. Штили?
Готово. Флажки?
Готово. Разнесённость флажков можно улучшить (что, наверно, можно сделать при помощи margin), но с позиционированием всё нормально.
Если засунуть несколько таких тактов в контейнер flexbox, то мы получим адаптивную нотную запись:
Очевидно, что здесь ещё многого не хватает, но основание заложено. Результат уже рендерится красивее, чем в других онлайн-рендерерах музыки.
Пока не будем обращать внимания на вязки и отметим, что головки нот, которые ближе по времени друг к другу, рендерятся чуть ближе друг к другу:
Это сделано намеренно при помощи небольшого
Постоянство расстояний можно контролировать регулировкой margin символов. Чтобы расстановка была более постоянной, мы уменьшим
Но это выглядит некрасиво, потому что интервалы между головками не дают читателю никакого представления о том, насколько быстр ритм. Однако в CSS есть удобный способ управления метриками. И теперь наша цель — настроить эти метрики, чтобы повысить читаемость.
Возможно, вы задаётесь вопросом, почему я использовал для горизонтальных и вертикальных интервалов отдельные классы, а не один? Разделив оси, мы можем заменить одну, не касаясь другой. Возьмём для примера такую мелодию:
Чтобы отобразить ту же мелодию в басовом ключе, можно заменить класс
Или если сопоставить
Разумеется, я упростил объяснение. Не всё заканчивается сменой класса, необходимо также изменить расположение штилей и добавочных линеек.
Вот класс нотного стана, полностью меняющий сопоставление высот нот. В General MIDI голоса ударных инструментов находятся в группе нот в нижних октавах клавиатуры, но эти ноты не связаны с тем, где ударные печатаются на нотном стане. Можно определить в CSS класс
Получилась очень читаемая нотация ударных, я очень ею доволен.
CSS Grid позволяет выравнивать в сетке нотации и другие символы. С временными событиями можно выравнивать, например, аккорды, тексты и динамику:
Вязки, аккорды и длинные паузы преобразуются в столбцы со span сопоставлением их атрибутов
Вся система имеет размер
Идеальна ли эта система? Честно говоря, я поражён тем, насколько хорошо она работает, но если уж искать недостатки, то…
1. CSS не может автоматически располагать новый символ ключа в начале каждой перенесённой строки
2. Он не может связать головку ноты с новой головкой в новой строке.
3. Вязки под углом — это совершенно отдельная история; вязки 1/16-х и 1/32-х нот сложно выровнять, потому что мы не знаем точно, где будут их штили, пока их не разместит Grid:
Так что для полного завершения работы потребуется немного JavaScript, но основную работу по размещению элементов здесь выполняет CSS, а значит, для JavaScript остаётся довольно мало труда.
Специальный элемент для рендеринга музыки
Репозиторий кода: github.com/stephband/scribe/
Формат данных Scribe: github.com/soundio/music-json/
Я написал интерпретатор для этой новой системы CSS и обернул его в элемент
Элемент
Или из файла, полученного в атрибуте
Или из объекта JS, указанного в свойстве
Основная документация по всему этому есть в README.
Можно протестировать текущую dev-сборку, импортировав в веб-страницу следующие файлы:
Как я сказал, проект пока в разработке. В дальнейшем я хочу исследовать и попробовать реализовать следующие функции:
В вебе нотная запись должна быть столь же доступной и плавной, как текст; однако пока это не так, и это уязвляет мои чувства. Давайте решим эту актуальную проблему.
Прототип Scribe
SVG, отрендеренный Scribe 0.2
Несколько лет назад я создал прототип рендерера музыки, который назвал Scribe. Он выполняет преобразование JSON в SVG. Изначально я стремился к созданию адаптивного рендерера музыки. Это было хорошее демо, но для дальнейшего развития пришлось бы писать сложный многопроходный движок генерации макетов, а у меня тогда возникли другие дела.
Вскоре после этого я занялся адаптированием Grid под проекты компании, и тут мне почудилось нечто знакомое: я задался вопросом, а не станет ли он решением некоторых проблем, с которыми я столкнулся при разработке Scribe?
Класс .stave
Нотный стан выстроен в подобие сетки. Высота ноты откладывается по вертикальной оси, а время идёт влево по горизонтальной оси. Я определю эти две оси в двух отдельных классах. Вертикальная ось, описывающая строки сетки, будет называться
.stave
. К оси времени мы вернёмся чуть позже..stave
содержит строки сетки фиксированного размера, имеющие имена стандартных высот нот, и фоновое изображение, отрисовывающее стан. То есть для нотных линеек скрипичного ключа map строк может выглядеть так:.stave {
display: grid;
row-gap: 0;
grid-template-rows:
[A5] 0.25em [G5] 0.25em [F5] 0.25em [E5] 0.25em
[D5] 0.25em [C5] 0.25em [B4] 0.25em [A4] 0.25em
[G4] 0.25em [F4] 0.25em [E4] 0.25em [D4] 0.25em
[C4] 0.25em ;
background-image: url('/path/to/stave.svg');
background-repeat: no-repeat;
background-size: 100% 2.25em;
background-position: 0 50%;
}
Если применить этот код к
<div>
, то получим следующее:Неплохо. Не особо информативно, но, изучив результат, мы увидим, что у каждой линейки и у каждого пустого поля теперь есть собственная строка сетки с названием высоты ноты, обозначающая каждую строку:
▍ Размещаем высоты нот на нотном стане
Каждая из строк стана может содержать одну из нескольких высот нот. Например, ноты G♭, G и G♯ должны находиться на одной линейке G.
Чтобы разместить описывающие эти ноты элементы DOM в нужные строки, я помещу названия нот в атрибуты
data-pitch
и использую CSS, чтобы сопоставить значения data-pitch
со строками линеек..stave > [data-pitch^="G"][data-pitch$="4"] { grid-row-start: G4; }
Это правило обрабатывает ноты, начинающиеся с
'G'
и заканчивающиеся на '4'
, то есть оно присваивает ноты 'G♭4'
, 'G4'
и 'G♯4'
(а также дубль-бемоль 'G?4'
и дубль-диез 'G?4'
) строке G4
. Это необходимо проделать для каждой строки нотного стана:.stave > [data-pitch^="A"][data-pitch$="5"] { grid-row-start: A5; }
.stave > [data-pitch^="G"][data-pitch$="5"] { grid-row-start: G5; }
.stave > [data-pitch^="F"][data-pitch$="5"] { grid-row-start: F5; }
.stave > [data-pitch^="E"][data-pitch$="5"] { grid-row-start: E5; }
.stave > [data-pitch^="D"][data-pitch$="5"] { grid-row-start: D5; }
...
.stave > [data-pitch^="D"][data-pitch$="4"] { grid-row-start: D4; }
.stave > [data-pitch^="C"][data-pitch$="4"] { grid-row-start: C4; }
И этого будет достаточно, чтобы начать размещать символы на нотном стане! У меня есть SVG-символы, которые я подготовил для прототипа Scribe. Давайте попробуем поместить парочку на стан:
<div class="stave">
<svg data-pitch="G4" class="head">
<use href="#head[2]"></use>
</svg>
<svg data-pitch="E5" class="head">
<use href="#head[2]"></use>
</svg>
</div>
Выглядит многообещающе. Теперь займёмся временем.
Класс .bar и его такты
С ритмом, пожалуй, работать сложнее. Не сразу понятно, какую выбрать наименьшую часть ритма, поддерживающую все возможные ритмы. Необходимо принять решение о том, какие минимальные длительности нот и какие кросс-ритмы нужно поддерживать в рамках сетки.
Если разделить такт на 24 столбца, то мы можем равномерно распределять восьмые (12 столбцов), шестнадцатые (6 столбцов), 32-е (3 столбца), а также значения триолей этих нот. Неплохо для начала.
Вот четырёхдольный такт, определённый как 4 × 24 = 96 столбцов сетки, плюс по столбцу в начале и в конце:
.bar {
column-gap: 0.03125em;
grid-template-columns:
[bar-begin]
max-content
repeat(96, minmax(max-content, auto))
max-content
[bar-end];
}
Добавим пару тактовых черт как контент
::before
и ::after
, а затем добавим символ ключа, отцентрированный на стане при помощи data-pitch="B4"
, и получим следующее:<div class="stave bar">
<svg data-pitch="B4" class="treble-clef">
<use href="#treble-clef"></use>
</svg>
</div>
При внимательном изучении можно заметить, что ключ попал в первый столбец, и что есть 96 столбцов нулевой длины, по 24 на долю, каждый из которых разделён небольшим
column-gap
:▍ Размещение символов в долях
Теперь я воспользуюсь атрибутами
data-beat
, чтобы присвоить доле элементы, а также применю правила CSS для сопоставления долей со столбцами сетки. После создания правила для каждой 1/24-й доли CSS map выглядит так:.bar > [data-beat^="1"] { grid-column-start: 2; }
.bar > [data-beat^="1.04"] { grid-column-start: 3; }
.bar > [data-beat^="1.08"] { grid-column-start: 4; }
.bar > [data-beat^="1.12"] { grid-column-start: 5; }
.bar > [data-beat^="1.16"] { grid-column-start: 6; }
.bar > [data-beat^="1.20"] { grid-column-start: 7; }
.bar > [data-beat^="1.25"] { grid-column-start: 8; }
...
.bar > [data-beat^="4.95"] { grid-column-start: 97; }
Селектор атрибута
^=
делает правило устойчивым к ошибкам. Рано или поздно неокруглённые числа или числа с плавающей запятой неизбежно отрендерятся в data-beat
. Двух десятичных знаков после запятой достаточно для идентификации 1/24-й доли на столбец сетки.Соединив это с классом
stave
, мы сможем размещать символы в зависимости от их высоты и доли, присваивая data-beat
значение доли от 1
до 5
, а data-pitch
имя ноты. В процессе столбцы долей, содержащие эти символы, будут адаптироваться под них:<div class="stave bar">
<svg class="clef" data-pitch="B4">…</svg>
<svg class="flat" data-beat="1" data-pitch="Bb4">…</svg>
<svg class="head" data-beat="1" data-pitch="Bb4">…</svg>
<svg class="head" data-beat="2" data-pitch="D4">…</svg>
<svg class="head" data-beat="3" data-pitch="G5">…</svg>
<svg class="rest" data-beat="4" data-pitch="B4">…</svg>
</div>
Отлично. Штили?
Готово. Флажки?
Готово. Разнесённость флажков можно улучшить (что, наверно, можно сделать при помощи margin), но с позиционированием всё нормально.
Плавная и адаптивная нотация
Если засунуть несколько таких тактов в контейнер flexbox, то мы получим адаптивную нотную запись:
<figure class="flex">
<div class="treble-stave stave bar">…</div>
<div class="treble-stave stave bar">…</div>
<div class="treble-stave stave bar">…</div>
…
</figure>
Очевидно, что здесь ещё многого не хватает, но основание заложено. Результат уже рендерится красивее, чем в других онлайн-рендерерах музыки.
Пространство между нотами
Пока не будем обращать внимания на вязки и отметим, что головки нот, которые ближе по времени друг к другу, рендерятся чуть ближе друг к другу:
Это сделано намеренно при помощи небольшого
column-gap
. Сами столбцы имеют нулевую ширину, если в них нет головки ноты, но между событиями есть другие column-gap (по 24 на долю), которые в долях расположены дальше друг от друга, поэтому расстояние увеличивается.Постоянство расстояний можно контролировать регулировкой margin символов. Чтобы расстановка была более постоянной, мы уменьшим
column-gap
, увеличив margin головок нот:Но это выглядит некрасиво, потому что интервалы между головками не дают читателю никакого представления о том, насколько быстр ритм. Однако в CSS есть удобный способ управления метриками. И теперь наша цель — настроить эти метрики, чтобы повысить читаемость.
▍ Ключи и обозначения размеров
Возможно, вы задаётесь вопросом, почему я использовал для горизонтальных и вертикальных интервалов отдельные классы, а не один? Разделив оси, мы можем заменить одну, не касаясь другой. Возьмём для примера такую мелодию:
Чтобы отобразить ту же мелодию в басовом ключе, можно заменить класс
stave
классом bass-stave
, сопоставляющим те же атрибуты data-pitch
с басовым нотным станом:<div class="bass-stave bar">...</div>
Или если сопоставить
data-duration="5"
с 120 grid-template-columns
в .bar
, то тому же нотному стану можно присвоить размер 5/4:<div class="bass-stave bar" data-duration="5">...</div>
Разумеется, я упростил объяснение. Не всё заканчивается сменой класса, необходимо также изменить расположение штилей и добавочных линеек.
Вот класс нотного стана, полностью меняющий сопоставление высот нот. В General MIDI голоса ударных инструментов находятся в группе нот в нижних октавах клавиатуры, но эти ноты не связаны с тем, где ударные печатаются на нотном стане. Можно определить в CSS класс
drums-stave
, сопоставляющий эти ноты с нужными строками:<div class="drums-stave bar" data-duration="4">...</div>
<div class="percussion-stave bar" data-duration="4">...</div>
Получилась очень читаемая нотация ударных, я очень ею доволен.
▍ Аккорды и текст
CSS Grid позволяет выравнивать в сетке нотации и другие символы. С временными событиями можно выравнивать, например, аккорды, тексты и динамику:
▍ Но что насчёт вязок?
Вязки, аккорды и длинные паузы преобразуются в столбцы со span сопоставлением их атрибутов
data-duration
со значениями span grid-column-end
:.stave > [data-duration="0.25"] { grid-column-end: span 6; }
.stave > [data-duration="0.5"] { grid-column-end: span 12; }
.stave > [data-duration="0.75"] { grid-column-end: span 18; }
.stave > [data-duration="1"] { grid-column-end: span 24; }
.stave > [data-duration="1.25"] { grid-column-end: span 30; }
...
▍ Размеры
Вся система имеет размер
em
, так что для её масштабирования достаточно просто изменить font-size
:▍ Ограничения Flex и Grid
Идеальна ли эта система? Честно говоря, я поражён тем, насколько хорошо она работает, но если уж искать недостатки, то…
1. CSS не может автоматически располагать новый символ ключа в начале каждой перенесённой строки
2. Он не может связать головку ноты с новой головкой в новой строке.
3. Вязки под углом — это совершенно отдельная история; вязки 1/16-х и 1/32-х нот сложно выровнять, потому что мы не знаем точно, где будут их штили, пока их не разместит Grid:
Так что для полного завершения работы потребуется немного JavaScript, но основную работу по размещению элементов здесь выполняет CSS, а значит, для JavaScript остаётся довольно мало труда.
<scribe-music>
Специальный элемент для рендеринга музыки
▍ Scribe
Репозиторий кода: github.com/stephband/scribe/
▍ JSON
Формат данных Scribe: github.com/soundio/music-json/
Я написал интерпретатор для этой новой системы CSS и обернул его в элемент
<scribe-music>
. Он ещё далёк от готовности, но уже способен рендерить адаптивный нотный лист. Мне кажется, это интересный и полезный проект.▍ Что он делает?
Элемент
<scribe-music>
рендерит музыкальную нотацию из данных, найденных в её содержимом:<scribe-music type="sequence">
0 chord D maj 4
0 F#5 0.2 4
0 A4 0.2 4
0 D4 0.2 4
</scribe-music>
Или из файла, полученного в атрибуте
src
, например, из этого JSON:<scribe-music
clef="drums"
type="application/json"
src="/static/blog/printing-music/data/caravan.json">
</scribe-music>
Или из объекта JS, указанного в свойстве
.data
элемента.Основная документация по всему этому есть в README.
▍ Попробовать самостоятельно
Можно протестировать текущую dev-сборку, импортировав в веб-страницу следующие файлы:
<link rel="stylesheet" href="https://stephen.band/scribe/scribe-music/module.css" />
<script type="module" src="https://stephen.band/scribe/scribe-music/module.js"></script>
Как я сказал, проект пока в разработке. В дальнейшем я хочу исследовать и попробовать реализовать следующие функции:
- Поддержку шрифтов SMuFL — смену шрифта, используемого для символов нотации. Пока мне удаётся стабильным образом отображать их расширенные наборы символов в разных браузерах.
- Поддержку вложенных последовательностей, что позволит рендерить мелодии из нескольких партий.
- Рендеринг разделённого стана — размещение нескольких партий на одном стане. Половина механики уже готова: нотация ударных и нотация пианино автоматически разделяются по высотам нот.
- Рендеринг нескольких станов — размещение нескольких партий на нескольких выровненных станах.
Telegram-канал со скидками, розыгрышами призов и новостями IT ?