paint-brush
Взятие аккорда: искусство оживлять музыку смысломк@charnog
331 чтения
331 чтения

Взятие аккорда: искусство оживлять музыку смыслом

к Denis Gonchar10m2023/10/18
Read on Terminal Reader

Слишком долго; Читать

Сочетая страсть к музыке с техническим опытом, эта статья знакомит читателей с созданием уникального визуального веб-сайта альбома. Освещаются проблемы, упрощения кодирования и роль протокола Open Graph, при этом подчеркивается баланс между перфекционизмом и быстрым выполнением.
featured image - Взятие аккорда: искусство оживлять музыку смыслом
Denis Gonchar HackerNoon profile picture
0-item

Обложка альбома DALL·E 3


В этой статье я расскажу, как за выходные я разработал проект по выпуску своего альбома ( https://evhaevla.netlify.app/ ). Я не профессиональный музыкант и не композитор, но иногда в моей голове всплывают мелодии. Я записываю их, а затем позволяю компьютеру воспроизвести.


В 2021 году я выпустил свой альбом под названием «Все счастливы, все смеются». Это простой альбом незнакомого «композитора» – это я.


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


В этой статье не будут углубляться во все технические детали — она была бы слишком длинной и могла бы понравиться не всем. Вместо этого я выделю основные концепции и препятствия, с которыми я столкнулся. Для интересующихся весь код можно найти на GitHub .

Визуальный

Мой альбом написан для фортепиано, что делает решение простым. Представьте себе прямоугольники, нисходящие на клавиши фортепиано. Любой, у кого есть музыкальные наклонности, наверняка встречал на YouTube множество видеороликов, изображающих ноты в такой манере. Прямоугольник касается клавиши, подсвечивая ее и указывая точный момент удара по ноте.


Я не уверен в происхождении этого визуального стиля, но быстрый поиск в Google в основном выдаёт скриншоты Synthesia.

Пользовательский интерфейс Синтезии

На YouTube есть авторы, которым удается создавать визуально ошеломляющие эффекты. Просмотр таких видео – удовольствие как с эстетической, так и с музыкальной точки зрения. Посмотрите то или это .


Что нам нужно будет реализовать?

  1. Ключи
  2. Прямоугольники
  3. Аудио
  4. Анимация


Давайте разберемся с каждым пунктом и приведем все это в действие.

Ключи

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


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

 <template> <ul style="transform: translate3d(0, 0, 0)"> <li :id="`key_${OFFSET - 1}`" style="display: none"></li> <template v-for="key in keys" :key="key.number"> <li :class="`${key.color} ${key.name}`" :id="`key_${key.number}`"></li> </template> </ul> </template> <script setup lang="ts"> import { ref } from 'vue' import type { Key } from '@/components/types' const OFFSET = 24 const template = [ { color: 'white', name: 'c' // Do }, { color: 'black', name: 'cs' // Do-diez }, { color: 'white', name: 'd' // Re }, /* ... */ ] const keys = ref<Key[]>([]) for (let i = 0; i < 72; i++) { keys.value.push({ ...template[i % 12], number: i + OFFSET }) } </script>

Я хотел бы отметить, что я добавил атрибут id к каждой клавише, который будет необходим при запуске анимации.

Прямоугольники

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


  1. Как можно добиться эффекта нисходящих нот?


  2. Есть ли необходимость поддерживать структуру, к которой можно обращаться для получения текущих заметок?


  3. Каков наилучший подход к отображению результатов таких запросов?


Каждый вопрос представляет собой препятствие для беспрепятственного достижения желаемого эффекта.


Я не буду задерживаться на каждом вопросе, а сразу перейду к делу. Учитывая множество проблем, связанных с динамическим подходом, разумно прислушаться к бритве Оккама и выбрать статическое решение.


Вот как я решил эту проблему: я отрисовал все 6215 нот одновременно на одном обширном холсте. Этот холст размещен внутри контейнера, стилизованного под свойство overflow: hidden . Для достижения эффекта падающих нот достаточно просто анимировать scrollTop этого контейнера.


Однако остается нерешенный вопрос: как мне получить координаты каждой ноты?


К счастью, у меня есть MIDI-файл, в котором заархивированы все эти ноты, — удобство, которое дает мне возможность быть композитором альбома. Все сводится к рендерингу нот с использованием данных, извлеченных из MIDI-файла.


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


Библиотека midi-file эффективно извлекает необработанные данные из MIDI-файлов, но для моих нужд этого недостаточно. Я стремлюсь преобразовать эти данные в более доступный и удобный для приложений формат, чтобы обеспечить плавный рендеринг в приложении.


В MIDI-файле речь идет не о нотах в привычном понимании, а о событиях. Существует целый ряд таких событий, но я в первую очередь сосредоточен на двух типах: «noteOn», которое срабатывает при нажатии клавиши, и «noteOff», когда клавиша отпускается.


События noteOn и noteOff указывают конкретный номер ноты, которая была нажата или отпущена соответственно. Время в общепринятом понимании в MIDI отсутствует. Вместо этого у нас есть «галочки». Количество тиков на долю подробно указано в заголовке MIDI-файла.

Синий — доли (длина по умолчанию — 96 тактов), Красный — события нот.


Действительно, есть еще над чем подумать. Также присутствует дорожка темпа, содержащая события setTempo, которые являются неотъемлемой частью процесса, учитывая, что темп может меняться во время воспроизведения. Мой первоначальный подход заключался в настройке скорости анимации свойства scrollTop контейнера в соответствии с темпом.


Однако вскоре я понял, что это не даст ожидаемого результата из-за чрезмерного накопления ошибок. «Линейное» растяжение времени оказалось более эффективным для анимации scrollTop .


Даже после того, как аспект анимации был отсортирован, темп по-прежнему требовал учета. Я решил эту проблему, отрегулировав длину самих прямоугольников нот. Хотя этот метод и не был оптимальным решением (идеальным было бы управление скоростью), этот метод обеспечивал более плавную работу.


Это решение не идеально, главным образом потому, что я связываю событие темпа с событием ноты в зависимости от того, имеют ли они одинаковое или меньшее время. Это означает, что если произойдет другое событие темпа, пока нота все еще находится в состоянии воспроизведения, оно будет просто проигнорировано.


Это потенциально может привести к ошибке, особенно если нота очень длинная и во время ее воспроизведения происходит резкое изменение темпа. Это компромисс. Я принял этот незначительный недостаток, поскольку сосредоточен на быстром развитии.


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

С нотой связано только первое событие темпа.


Итак, мы располагаем следующей информацией:


  1. Конкретный ключевой номер
  2. Точный момент нажатия клавиши
  3. Точное время отпускания ключа
  4. Интенсивность или «скорость» нажатия клавиши.


Имея под рукой эти детали, я могу определить точные координаты каждой ноты на холсте. Номер клавиши определяет ось X, а начало нажатия клавиши — ось Y. Длина пресса определяет высоту прямоугольника.


Используя стандартный элемент div и установив для него «абсолютное» положение, я успешно добился желаемого эффекта.

Прямоугольники, которые вы видите здесь, представляют собой простые элементы `<div>` с примененными стилями. Они располагаются абсолютно на вытянутом холсте, который затем прокручивается.


Аудио

Я не собирался создавать синтезатор для фортепиано, так как это заняло бы много времени. Вместо этого я использовал существующий файл OGG, который уже был «рендерен», и выбрал The Grandeur от Native Instruments в качестве звуковой библиотеки.


Лично я считаю, что это лучший фортепианный VST-инструмент, доступный на рынке.


Я встроил полученный файл OGG в стандартный аудиоэлемент. Моей основной задачей тогда была синхронизация звука с анимацией scrollTop моего холста заметок.

Анимация

Прежде чем я смог заняться синхронизацией, сначала нужно было установить анимацию. Анимация холста довольно проста — я анимирую scrollTop от бесконечного значения до нуля, используя линейную интерполяцию. Продолжительность этой анимации соответствует длине альбома.


Когда нота попадает на клавишу, эта клавиша подсвечивается. Это означает, что для спуска каждой ноты мне нужно «активировать» соответствующую клавишу, а как только нота завершит свой ход, деактивировать ее.


Всего 6215 заметок, что соответствует колоссальным 12 430 анимациям активации и деактивации заметок.


Кроме того, я стремился предоставить пользователям возможность перематывать аудио, позволяя им перемещаться в любом месте альбома. Для реализации такой функции необходимо надежное решение.


И когда я сталкиваюсь с необходимостью надежного решения, которое «просто работает», я всегда обращаюсь к платформе GreenSock Animation Platform .


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


 const keysTl = gsap.timeline() notes.value.forEach((note) => { const keySelector = `#note_${note.noteNumber}` keysTl .set(keySelector, KEY_ACTIVE_STATE, note.positionSeconds) .set(keySelector, KEY_INACTIVE_STATE, note.positionSeconds + note.durationSeconds - 0.02) })


Код синхронизации по существу устанавливает связь через события между звуком и глобальной временной шкалой GSAP.


 audioRef.value?.addEventListener('timeupdate', () => { const time = audioRef.value?.currentTime ?? 0 globalTl.time(time) }) audioRef.value?.addEventListener('play', () => { globalTl.play() }) audioRef.value?.addEventListener('playing', () => { globalTl.play() }) audioRef.value?.addEventListener('waiting', () => { globalTl.pause() }) audioRef.value?.addEventListener('pause', () => { globalTl.pause() })


Подписи

Когда мне захотелось подвести итоги, мне в голову пришла интригующая идея. Что, если я добавлю в альбом уникальную особенность? Изначально этого не было в моем списке дел, но я чувствовал, что без этой функции проект не будет по-настоящему блестящим. Поэтому я решил включить и его.


Каждый раз, когда я погружаюсь в трек, я размышляю о его более глубоком значении. Какое послание пытался передать композитор? Возьмем, к примеру, отрывок из «Ночной тетради» Людовико Эйнауди. Фортепиано резонирует в левом ухе, а струны — в правом.


Это создает атмосферу диалога, разворачивающегося между ними. Такое ощущение, будто клавиши пианино щупают: «Вы согласны?» Струны отвечают утвердительно. «Это вопрос?» Струны повторяют их подтверждение. Кульминацией композиции является сближение обоих инструментов, символизирующее реализацию единства и гармонии. Разве это не завораживающий опыт?


Необходимо отметить, что это сугубо моя личная интерпретация. Однажды мне довелось побывать на концерте Людовико в Милане. После спектакля я подошел к нему и поинтересовался, действительно ли он намеревался внедрить идею диалога в этот конкретный фрагмент.


Его ответ был поучительным: «Я никогда не думал об этом таким образом, но у вас определенно яркое воображение».


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


Эта функция может предложить слушателям более глубокое понимание или свежий взгляд на то, «что на самом деле имел в виду автор?»


Мне повезло, что я выбрал GSAP в качестве инструмента для анимации. Это позволило мне без особых усилий интегрировать еще одну временную шкалу, специально предназначенную для анимации комментариев. Это дополнение упростило процесс и сделало реализацию моей идеи намного более гладкой.


У меня была склонность вводить комментарии с помощью HTML-разметки. Для этого я создал компонент, который представляет анимацию во время события onMounted .

 <template> <div :class="$style.comment" ref="commentRef"> <slot></slot> </div> </template> <script setup lang="ts"> /* ... */ onMounted(() => { if (!commentRef.value) return props.timeline .fromTo( commentRef.value, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.5 }, props.time ? parseTime(props.time) : props.delay ? `+=${props.delay}` : '+=1' ) .to( commentRef.value, { autoAlpha: 0, duration: 0.5 }, props.time ? parseTime(props.time) + props.duration : `+=${props.duration}` ) }) </script>


Использование этого компонента будет следующим.

 <template> <div> <Comment time="0:01" :duration="5" :timeline="commentsTl"> <h1>A title for a track</h1> </Comment> <Comment :delay="1" :duration="13" :timeline="commentsTl"> I would like to say... </Comment>

Апофеоз

Когда все элементы были готовы, следующим шагом было размещение сайта на хостинге. Я выбрал Netlify. А теперь я приглашаю вас познакомиться с альбомом и посмотреть финальную презентацию.


https://evhaevla.netlify.app


Я искренне надеюсь, что найдутся и другие разработчики, любящие фортепиано, которые хотят продемонстрировать свои альбомы в такой уникальной манере. Если вы один из них, не стесняйтесь создавать форк проекта.