Статьи

Создайте Music Jam Station с помощью Vanilla JavaScript

Эта статья была рецензирована Крисом Перри , Микаэлой Лер и Мэттом Бернеттом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

Точное понимание основ Javascript необходимо для следования этому руководству.

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

См. Pen Jam Station от SitePoint ( @SitePoint ) на CodePen .

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

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

Что тебе понадобится

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

Есть несколько вещей, которые вам нужно знать о вашей звуковой дорожке, чтобы найти текущий ритм и такт.

  • Ударов в минуту (BPM или темп)
  • Время Подпись
  • Время начинается трек
  • Сколько мер используется для подсчета (не всегда применимо)

При желании вы можете использовать звуковую дорожку, которую я использовал в демонстрационной программе CodePen (щелкните правой кнопкой мыши, чтобы сохранить)

Вот соответствующие данные для этого трека.

  • BPM = 117
  • Время Подпись = 4/4
  • Время начала первого удара = 0,2 с
  • Меры, используемые для подсчета в = 1

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

Разметка и CSS

Прежде чем перейти к интересным вещам, вот HTML и CSS, которые мы будем использовать.

<div class="wrapper"> <div id="chord-diagram"></div> <audio id="jam-track" src="https://myguitarpal.com/wp-content/uploads/2014/09/12-Bar-Blues-in-A-Version-1.mp3" controls></audio> <br> <label>Beat: </label> <div class="data" id="beat"></div> <label>Measure: </label> <div class="data" id="measure"></div> <label>Section: </label> <div class="data" id="section"></div> <label>Chord: </label> <div class="data" id="chord"></div> <div id="chord-progression"></div> </div> 
 .wrapper { max-width: 400px; } #chord-progression { margin-top: 20px; padding-top: 20px; border-top: 1px solid lightgray; } .section { margin-bottom: 20px; display: none; } .measure { width: 25%; display: inline-block; } .m-line { float: right; width: 10px; } audio { width: 100%; } .data { display: inline; margin-right: 10px; } label { font-weight: bold; } 

Настройка переменных

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

 // beats per minute var BPM = 117; // beats per second var BPS = 60 / BPM; // measures used for count in var measuresCount = 1; // time the track starts var offsetSeconds = 0.2; // time signature var timeSigTop = 4; var timeSigBottom = 4; 

Аккордовая прогрессия

Для целей этого урока я сохранил модель данных последовательности аккордов довольно простой и использовал многомерный массив.

Массив верхнего уровня — это разделы последовательности аккордов. Это могут быть стихи, хоры, мосты и т. Д.

Вложенные массивы содержат аккорды для каждого такта в соответствующих разделах.

 var sectionOne = ['A7', 'A7', 'A7', 'A7', 'D7', 'D7', 'A7', 'A7', 'E7', 'D7', 'A7', 'E7']; var sectionTwo = ['A7', 'A7', 'A7', 'A7', 'D7', 'D7', 'A7', 'A7', 'E7', 'D7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7']; var chordProgression = [ sectionOne, sectionOne, sectionOne, sectionTwo ]; 

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

Запуск станции Jam

Эта станция джема использует событие timeupdate, которое похоже на использование игрового цикла. Каждый раз, когда timeupdate событие timeupdate (несколько раз в секунду во время воспроизведения трека), мы собираемся запустить функцию и обновить некоторые данные, такие как текущий ритм, такт и аккорд. Единственный раз, когда данные не будут обновляться, это когда дорожка приостановлена.

Когда timeupdate событие timeupdate мы jamStation функцию jamStation . Функция будет вызываться несколько раз в секунду во время воспроизведения звука.

 var audio = document.getElementById('jam-track'); audio.ontimeupdate = function() { jamStation(); }; 

Веселая часть

Эта функция должна работать только с данными, а не с представлением. Позже мы будем использовать другую функцию ( renderJamStation ) для работы с презентацией.

Чтобы найти текущий удар, мы будем использовать выражение (audio.currentTime - offsetSeconds + BPS) / BPS , и мы сохраним это значение в переменной beat .

Затем мы можем найти текущую меру с помощью выражения (beat - 1) / timeSigTop , которое мы будем хранить в переменной measure .

 function jamStation() { var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; var measure = (beat - 1) / timeSigTop; } 

Переменные beat и measure теперь должны быть округлены в меньшую сторону. Это даст нам более простые цифры для работы, когда мы будем делать сравнения. Однако я бы не советовал делать это, если бы вы делали более сложное приложение, в котором вам нужно было использовать доли удара.

Мы будем хранить эти целые числа в cleanBeat и cleanMeasure .

Если вы хотите отобразить текущий удар в такте, вы можете использовать следующее выражение: ((cleanBeat - 1) % timeSigTop) + 1 . Допустим, мы на ритме 13. Значения выражений будут: ((13 - 1) % 4) + 1 . Мы добавляем 1, потому что нет такой вещи, как удар 0.

Теперь вместо бесконечного приращения ритма он будет увеличиваться только до числа в верхней части временной сигнатуры. measureBeat будет содержать значение доли в пределах меры. Таким образом, cleanMeasure будет рассчитывать 1,2,3,4,5,6,7,8 и т. Д., А measureBeat будет рассчитывать 1,2,3,4,1,2,3,4 и т. Д.

 function jamStation() { var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; var measure = (beat - 1) / timeSigTop; // round down for beat and measure var cleanBeat = Math.floor(beat); cleanMeasure = Math.floor(measure); // find the current beat within the measure measureBeat = ((cleanBeat - 1) % timeSigTop) + 1; } 

Не помещайте var перед cleanMeasure как мы уже объявили эту переменную вне функции (см. CodePen ), поскольку мы хотим получить доступ к этому значению извне jamStation() .

На данный момент у нас есть самые важные данные, которые нам нужны. Сложная часть, однако, будет иметь дело с развитием аккордов.

Следующая часть функции jamStation предназначена для определения двух вещей: currentSection и currentChord .

Сначала нам нужно использовать оператор if… else для проверки, если мы прошли первую меру. Если мы, мы найдем значение текущего раздела и текущего аккорда. Если нет, то currentSection и currentChord будут установлены в null.

Чтобы найти текущий раздел и текущий аккорд, мы перебираем разделы, а затем запускаем вложенный цикл, который перебирает меры в каждом разделе.

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

Не используйте var перед currentSection или currentChord , так как, опять же, эти переменные были объявлены вне jamStation() поэтому мы можем делиться ими между функциями.

 function jamStation() { var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; var measure = (beat - 1) / timeSigTop; // round down for beat and measure var cleanBeat = Math.floor(beat); cleanMeasure = Math.floor(measure); // find the current beat within the measure measureBeat = ((cleanBeat - 1) % timeSigTop) + 1; if (cleanMeasure > 0) { // find the currentSection and currentChord var count = 0; var br = false; for (var s = 0; s < chordProgression.length; s++) { for (var m = 0; m < chordProgression[s].length; m++) { count++; if (cleanMeasure == count) { currentSection = s + 1; currentChord = chordProgression[s][m]; br = true; break; } } if (br === true) { break; } } } else { currentSection = null; currentChord = null; } // display the jam station and its data renderJamStation(); } 

Теперь у нас есть все данные, которые нам нужны, и они доступны по всему миру.

В конце функции jamStation вы можете увидеть, что renderJamStation() запущен. Эта функция будет использоваться только в целях презентации, и мы расскажем о ней чуть позже.

Рендеринг последовательности аккордов

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

Сначала мы перебираем разделы в последовательности аккордов. Мы создаем div с классом «section» и id «section- [number]» каждый раз, когда цикл зацикливается. Каждый раздел имеет уникальный идентификатор, поэтому мы можем отображать этот конкретный раздел во время его воспроизведения.

 // take the chordProgression array and render the HTML function renderChordProgression() { var progression = document.getElementById('chord-progression'); // make the sections for (var s = 0; s < chordProgression.length; s++) { progression.innerHTML += '<div class="section" id="section-' + (s + 1) + '"></div>'; } } 

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

Вы, вероятно, заметили, что переменная count была установлена ​​и увеличена. Это делается для того, чтобы увеличивать каждую меру в каждом разделе.

Полная функция выглядит так:

 // take the chordProgression array and render the HTML function renderChordProgression(){ var progression = document.getElementById('chord-progression'); // make the sections for (var s = 0; s < chordProgression.length; s++){ progression.innerHTML += '<div class="section" id="section-' + (s + 1) + '"></div>'; } var count = 0; for (var s = 0; s < chordProgression.length; s++) { for (var m = 0; m < chordProgression[s].length; m++) { count++; var section = document.getElementById('section-' + (s + 1)); section.innerHTML += '<div class="measure" id="measure-' + count + '">' + chordProgression[s][m] + '<div class="m-line">|</div></div>'; } } } 

Возможно, вы захотите стилизовать последовательность аккордов. Вы можете ссылаться на CSS в CodePen в верхней части этого поста.

презентация

Функция renderJamStation вызывается из функции jamStation .

 function renderJamStation() { // show the beat within the measure, not overall document.getElementById('beat').innerHTML = measureBeat; // show the current measure, but only if the jam track is past the count in measures var measureElem = document.getElementById('measure'); // only show the current measure if it's > 0 if (cleanMeasure > 0) { measureElem.innerHTML = cleanMeasure; } else { measureElem.innerHTML = ''; } // show the section number document.getElementById('section').innerHTML = currentSection; // show the current chord name document.getElementById('chord').innerHTML = currentChord; // hide all sections before displaying only the section we want to see var allSections = document.getElementsByClassName('section'); for (var i = 0; i < allSections.length; i++) { allSections[i].style.display = 'none'; } // show the currently playing section if (currentSection != null) { document.getElementById('section-' + currentSection).style.display = 'block'; } else { allSections[0].style.display = 'block'; } // style the current chord in the chord progression if (cleanMeasure > 0) { // style all measures black var measures = document.getElementsByClassName('measure'); for (var i = 0; i < measures.length; i++) { measures[i].style.color = 'black'; } // style current measure red document.getElementById('measure-' + cleanMeasure).style.color = 'red'; } } 

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

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

 // hide all sections before displaying only the section we want to see var allSections = document.getElementsByClassName('section'); for (var i = 0; i < allSections.length; i++) { allSections[i].style.display = 'none'; } // show the currently playing section if (currentSection != null) { document.getElementById('section-' + currentSection).style.display = 'block'; } else { allSections[0].style.display = 'block'; } 

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

Чтобы отобразить текущий воспроизводимый раздел, мы выбираем его по его идентификатору и возвращаем свойство display обратно в block.

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

Визуализация начального отображения

Наконец, мы запускаем две функции. renderChordProgression() необходимо запустить один раз, чтобы отобразить последовательность аккордов, а jamStation() нужно запустить один раз. Да, функция jamStation() будет запускаться всякий раз, когда срабатывает timeupdate , но ее также следует запускать один раз самостоятельно, иначе последовательность аккордов не будет отображаться сначала.

 renderChordProgression(); jamStation(); 

Дальнейшие мысли и идеи

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

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

Вы можете создать массив объектов аккордов.

 var chords = [ { name: 'A', type: '7', src: '/images/a7.jpg', }, { name: 'D', type: '7', src: '/images/d7.jpg', }, { name: 'E', type: '7', src: '/images/e7.jpg', } ]; 

Затем запустите эту логику внутри функции jamStation которая покажет правильный аккорд в нужное время.

 for (var i = 0; i < chords.length; i++) { if (chords[i].name + chords[i].type == currentChord) { document.getElementById('chord-diagram').innerHTML = '<img src="' + chords[i].src + '"/>'; } } 

Вывод

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

  • Новый взгляд на галереи или слайдеры. Переход изображения на определенные удары или меры.
  • Анимировать изображения в такт. Постукивая ногами? Позы кого-то танцуют?
  • Аудио визуализации.
  • Аннотации или диаграммы, используемые в учебных целях, которые синхронизированы с музыкальным произведением.
  • С этим удивительным API нотной нотации с открытым исходным кодом JavaScript вы можете делать множество разных вещей.

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