Использование конечной точки анализа звука на платформе Spotify

Одной из ключевых особенностей моего недавнего проекта Future Islands был предварительный рендеринг волновых форм, чтобы у нас было классное визуальное сопровождение воспроизведения звука. Это тема, о которой я не думал с тех пор, как много лет назад работал в SoundCloud. Поскольку в этом проекте мы не транслировали звук из Spotify, в итоге я извлек данные формы волны с помощью Meyda. (См. Тематическое исследование для получения дополнительной информации.) Однако, когда я закончил проект, я начал думать о том, как на самом деле я мог бы создавать изображения формы волны из треков Spotify. Spotify на самом деле не предоставляет доступ к полноразмерным аудиофайлам (по уважительной причине), которые могут потребоваться для извлечения этих данных. Вдобавок, насколько я могу судить, их SDK веб-воспроизведения не раскрывает звук таким образом, чтобы вы могли сгенерировать его в реальном времени с помощью Веб-аудио.

Я был готов сдаться, когда вспомнил, что платформа Spotify предоставляет конечную точку для анализа звука. Эта конечная точка обеспечивает всевозможный интересный анализ структуры и музыкального содержания дорожки, включая ритм, высоту и тембр. Больше всего меня интересовали Сегменты. Это участки трека, которые содержат примерно одинаковый звук. Каждый сегмент имеет много интересных свойств, но меня больше всего интересовали три: начальная точка (в секундах), продолжительность (в секундах) и максимальная громкость (в децибелах) сегмента. Используя эти данные, я смогу визуализировать уровни звука дорожки. Сначала загрузим данные.

Вызов конечной точки

Поскольку я собираюсь упростить эти данные, прежде чем использовать их для визуализации, мне нравится использовать Curl для загрузки данных локально. Это можно сделать очень просто, передав идентификатор трека и токен доступа Spotify. Вы можете сгенерировать временный токен доступа с помощью консоли Spotify Platform. Как только вы это сделаете, просто добавьте --output track.json в команду curl, чтобы загрузить данные в файл.

curl -X "GET" "https://api.spotify.com/v1/audio-analysis/TRACK_ID" -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" --output track.json

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

Подготовка данных

Как я уже упоминал, конечная точка Audio Analysis предоставляет всевозможные интересные данные, и это делает размер возвращаемых данных довольно большим для работы с практическими приложениями. Я скачал новый трек Future Islands «Thrill», объем данных составил 378кб. 😰 Я хочу значительно упростить эти данные, включив в них только массив уровней громкости от 0 до 1. Затем я буду использовать этот массив уровней для генерации формы волны с использованием HTML5 Canvas или SVG. Я делаю это, написав небольшой скрипт узла.

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

const fs = require('fs')
const data = require('./track.json')

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

let duration = data.track.duration

Затем мы сопоставим данные сегментов, чтобы включить только свойства start, duration и loudness.

let segments = data.segments.map(segment => {
  let loudness = segment.loudness_max
  
  return {
    start: segment.start / duration,
    duration: segment.duration / duration,
    loudness: 1 - (Math.min(Math.max(loudness, -35), 0) / -35)
  }
})

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

Громкость немного сложнее, потому что она заявлена ​​в децибелах от 0 до -60. Подобно двум свойствам времени, я хочу превратить это в число с плавающей запятой от 0 до 1. Для этого мне нужно определить диапазон значений. Вы можете подумать, что простая установка диапазона от 0 до -60 даст хороший результат, но как часто треки Spotify достигают этих более низких значений в дБ? На конечной точке Spotify Audio Features они фактически используют диаграмму, которая показывает общее распределение данных в децибелах на платформе.

Используя эту диаграмму, я решил установить свой диапазон от 0 до -35. Без этого форма волны фактически имела бы много мертвого пространства для всех тех децибел, которые появляются нечасто. Затем вы можете использовать математические функции min и max с небольшим делением для создания того числа с плавающей запятой от 0 до 1, которое мы ищем.

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

let min = Math.min(...segments.map(segment => segment.loudness))
let max = Math.max(...segments.map(segment => segment.loudness))

Если вы внимательно посмотрите на данные сегмента, то заметите, что причина свойств start и duration заключается в том, что анализатор Spotify не анализирует треки хорошо согласованным образом, как каждый второй. Вместо этого эти сегменты организованы в фрагменты однородного звука. Мы хотим создать новый массив с именем levels и создать 1000 новых равномерно уровней звука от начала до конца дорожки.

Поскольку мы уже превратили наши свойства start и duration в числа с плавающей запятой, мы можем добиться этого, используя цикл for для увеличения на 1000 значений от 0 до 1. В каждой итерации цикла мы найдем сегмент, в котором текущий итератор duration падает и добавляет это значение громкости в массив. Однако мы не будем добавлять громкость напрямую, а вместо этого добавим еще один уровень упрощения и разделим значение громкости на максимальное значение на тот случай, если максимальное значение фактически не достигнет 1. О, и мы также округлим значение громкости. до двух знаков после запятой. Хорошо, хватит упрощения. 😅

let levels = []
for (let i = 0.000; i < 1; i += 0.001) {
  let s = segments.find(segment => {
    return i <= segment.start + segment.duration
  })
  let loudness = Math.round((s.loudness / max) * 100) / 100
  levels.push(
    loudness
  )
}

Последнее, что нужно сделать, это записать массив levels в файл JSON, чтобы мы использовали его для визуализации. Вы можете сделать это с помощью функции fs writeFile.

fs.writeFile('levels.json', JSON.stringify(levels), (err) => {
  console.log(err)
})

Это грубое решение, к которому я пришел быстро, и, безусловно, его можно немного исправить. А пока вот суть.

Генерация формы волны

В зависимости от того, где вы планируете использовать форму волны, существует множество способов их визуализации от SVG до Canvas. Лично я предпочитаю генерировать свои сигналы с помощью Canvas, чтобы они могли быть как динамическими, так и реагировать на размеры экрана. Обратите внимание на этот CodePen, который загружает наши данные и генерирует отзывчивую форму волны. Я немного объясню, что происходит.

Во-первых, я использую axios для загрузки данных осциллограммы, но вы можете использовать выборку, если хотите. Я устанавливаю метод Vue.js под названием renderWaveform для выполнения фактического рендеринга и обязательно вызываю его каждый раз при изменении размера окна.

let { data } = await axios.get('levels.json')
this.waveform = data
this.renderWaveform()
window.onresize = () => this.renderWaveform()

Наконец-то мы можем перейти к рендерингу этой вещи. Мне нравится помещать элемент <canvas> в адаптивный родительский div и просто изменять размер холста в зависимости от размера родительского элемента. Как только вы это сделаете, установите контекст для рисования.

let canvas = this.$refs.canvas
let { height, width } = canvas.parentNode.getBoundingClientRect()
canvas.width = width
canvas.height = height
let context = canvas.getContext('2d')

Затем нам нужно пройти через каждый пиксель ширины нашего холста и решить, следует ли и как рисовать там линию сигнала. В этом примере я собираюсь нарисовать линию шириной 4 пикселя через каждые 8 ​​пикселей, так как считаю это эстетичным. Я также решил отразить форму волны из центрированного вертикального положения, которое является обычным при визуализации формы волны.

for (let x = 0; x < width; x++) {
  if (x % 8 == 0) {
    let i = Math.ceil(this.waveform.length * (x / width))
    let h = Math.round(this.waveform[i] * height) / 2
    context.fillRect(x, (height / 2) - h, 4, h)
    context.fillRect(x, (height / 2), 4, h)
  }
}

Сначала я проверяю, делится ли значение x на 8. Если да, мы устанавливаем, какое значение из данных формы сигнала мы должны использовать для этой линии. Мы можем определить высоту этой линии, умножив выбранный уровень сигнала на высоту холста. Поскольку мы зеркально отражаем уровни по вертикали, мы уменьшим это значение вдвое. Наконец, мы нарисуем две линии, используя fillRect метод HTML Canvas. Один простирается от середины вверх, а другой - от середины вниз.

Опять же, проверьте и расколите эту ручку, если вам интересна эта тема.

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