Слишком часто я наблюдал за тем, как импровизирующий музыкант трясущимися руками пытается увеличить pdf размером A4 на крошечном экране телефона в самом разгаре исполнения. Мы обязаны создать плавный и отзывчивый рендеринг музыки для веба!

В вебе нотная запись должна быть столь же доступной и плавной, как текст; однако пока это не так, и это уязвляет мои чувства. Давайте решим эту актуальную проблему.

Прототип 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 ?

Комментарии (13)


  1. Daddy_Cool
    06.05.2024 13:54

    Ох! Полезно, но непонятно. И вопрос не задать.
    Итак... если я правильно понимаю...на входе нужно иметь музыку в формате
    0 chord D maj 4
    0 F#5 0.2 4
    0 A4 0.2 4
    0 D4 0.2 4
    и откуда её взять? Все пишут/набивают ноты в спецсофте типа MuseScore, Finale, и т.п... Оттуда можно экспортировать в midi или в какой-нибудь MusicXML (возможно, что в последнем это всё уже есть). Тогда нужен еще конвертер MusicXML->формат автора.
    К сожалению, нет единого редактируемого нотного формата - типа как doc, был бы - всё бы здорово упростилось.
    На днях скачал партитуру одной вещи для симфонического оркестра - в pdf. И в pdf-файле видно, что сделано в Finale, но самого файла в формате Finale, увы, нет. Теперь чтобы разобраться как и что там и послушать надо распознавать, редактировать, расставлять инструменты, и т.п...


    1. lanabel
      06.05.2024 13:54
      +3

      Справедливости ради, формат придуман не автором проекта, а существует как отдельный проект с замашкой на стандартизацию хранения музыки в json. Ссылка на гитхаб формата есть в статье. Но формат не вдохновляет и совершенно недоступен простому условному "музыканту", который хочет набрать пару строк. Нужен конвертер из abc или lilypond как минимум.

      Вообще, из всего, что умеет хранить ноты как текстовый файл мне больше всего зашел lilypond. Но он не решает задач, описанных в статье. Рассмотреный проект пока выглядит как интересный концепт для рендера нот для просмотра на устройствах разного размера. Пару лет назад искала нечто подобное, было всё печально. Но и тут слишком много ограничений, надо вникать, удастся ли адаптировать по свои задачи.


  1. voldemar_d
    06.05.2024 13:54
    +4

    А зачем такой, прямо скажем, плохо читаемый шрифт? Dmaj я еле прочитать смог.

    И у меня тот же вопрос - можно ли конвертировать, например, из формата MusicXML, в который большинство нотных редакторов экспортировать умеют?


    1. lanabel
      06.05.2024 13:54
      +1

      Быстрый поиск music xml to music json даёт пару многообещающих результатов, но надо проверять


  1. ALexKud
    06.05.2024 13:54
    +1

    Нет гитарных фичей , флажолетов, указателя повтора, апперджио и тп.


  1. gruzoveek
    06.05.2024 13:54
    +2

    Гениально. Есть куда расти, но идея офигенная


  1. Mausglov
    06.05.2024 13:54
    +1

    Я бы вынес диез и бемоль в отдельный дата атрибут (какой-нибудь data-pitchmod) - мне кажется, запись стала бы проще


  1. jellyfish
    06.05.2024 13:54

    Может имело смысл брать за основу не json, а abc notation? Для него и конвертеров полно готовых.


    1. lanabel
      06.05.2024 13:54

      Полезла разбираться с проектом, на гитхабе хорошая новость - Scribe 0.3 also parses ABC. Видимо в статью эта информация не успела попасть. Выглядит впечатляюще - создаём html файл, подрубаем js и css, вставляем набранный кусочек в ABC, получаем адаптивные ноты на странице с возможностью масштабирования. Вот пример из гитхаба проекта.

          <scribe-music type="text/x-abc" clef="treble">
              X: 1
              T: Sí Bheag Sí Mhór
              C: Turlough O'Carolan
              Z: ceolachan
              S: https://thesession.org/tunes/449#setting13324
              R: waltz
              M: 3/4
              L: 1/8
              K: Dmaj
              f3 e d2|d2 e2 d2|B4 A2|F4 A2|BA Bc d2|e4 de|f2 f2 e2|d4 f2|
              B4 e2|A4 d2|F2 F2 E2|D4 f2|B4 e2|A4 dc|d6-|d4:|
              f3 e d2|ed ef a2|b4 a2|f4 ed|e2 e2 a2|f4 e2|d4 B2|B4 A2|
              F4 E2|D4 f2|B4 e2|A4 a2|ba gf ed|e3 fe|d1
              |de:|
          </scribe-music>


      1. lanabel
        06.05.2024 13:54

        Дополню по результатам тестирования - поддержка ABC прям совсем начальная и сырая, почему-то не любит ключ C и ноту C. В проекте нет поддержки сложных размеров ни в одной из нотаций, размер 8/4 ломает всё, максимально можно выжать 7/4 или 14/8. Такт произвольной длины для упражнений создать нельзя. В общем и целом для моих специфических нужд не подойдёт в текущем состоянии, хотя можно попытаться адаптировать подход, но будет довольно трудоёмко.


  1. ZvoogHub
    06.05.2024 13:54
    +1

    не рассмотрен главный вопрос - производительность. Чтоб было понятно можно взять какое-то обычное произведение, например отсюда https://www.rusmidi.com/song/1558/ скачать MIDI В.Цой - Звезда по имени Солнце:

    гитара, бас, пианино, ударные - примерно по 8 нот на такт (у гитары больше), будем считать 50 нот на такт который при темпе 120 будет длительностью 2 секунды.

    Итого для 3-х минутного произведения нужно отобразить на экране 5 тыс. значков нот или более.

    Это много.


  1. youngmysteriouslight
    06.05.2024 13:54

    Что насчёт ключевых знаков? Как в этом случае размечаются ноты со случайной альтерацией и без? Бекар ставится автоматически?


  1. 3263927
    06.05.2024 13:54

    супер! спасибо огромное за такой отличный инструмент! надеюсь можно будет на скачивать ноты а просто на сайте смотреть. ещё надо будет сделать сенсорный датчик для переворота страниц и вобще всё полетит! <3<3<3