В этом руководстве вы познакомитесь с основами парсинга веб-страниц и ультразвуковой обработки данных с использованием Node.js и Tone.js. создание приложения, которое очищает графики вкладов пользователей GitHub и отображает их в виде пентатонических аккордов в тональности G.

Посмотреть в действии можно здесь: https://in-g.herokuapp.com/

См. Исходный код здесь: https://github.com/nielsenjared/in-g

В рамках этого руководства я предполагаю, что вы знакомы с Node.js, но не имеете опыта работы с веб-парсингом или веб-аудио. Если вы новичок в Node, вы можете загрузить и установить его здесь https://nodejs.org/en/. Есть бесчисленное множество руководств, которые помогут вам начать работу. Рекомендую MDN.

In G

Один из моих любимых альбомов - это In C Терри Райли. Это одно из самых ранних и самых известных произведений в стиле минимализм.

Графики участия пользователей GitHub очень похожи на цифровые звуковые секвенсоры, так почему бы не воспроизвести их как музыку? С графиками вкладов GitHub связано пять шестнадцатеричных значений, поэтому я основал звук на пентатонной шкале в тональности G. (GABDE, или do re mi so la). Каждый день недели - это октава, где воскресенье - самый высокий тон, а суббота - самый низкий.

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

Давай соскоблим!

Сторона сервера: парсинг веб-страниц с помощью Cheerio

Используя инструменты Inspect нашего браузера, мы можем увидеть, что граф пользователя GitHub отображается в <svg> элементе, содержащем вложенные <g> элементы, содержащие <rect> элементы. Обратите внимание, что есть семь <rect> элементов, и каждый имеет соответствующий атрибут data-date. Каждая неделя отображается в виде прямоугольников в столбце, начиная с воскресенья и заканчивая субботой. Мы хотим очистить атрибут fill, связанный с каждым <rect>.

Создайте новый каталог и перейдите в него.

mkdir in-g
cd in-g

Давайте сначала настроим простой скребок. Для этого нам нужны два пакета npm, cheerio и request.

npm install cheerio request

Затем создайте файл server.js и добавьте наши зависимости.

var cheerio = require("cheerio");
var request = require("request");

Мы используем request для HTTP-запросов и cheerio для выбора HTML-элементов на стороне сервера, как jQuery.

Давайте сначала сделаем запрос на GitHub.

request("https://github.com/nielsenjared", function(error, response, html) {
  console.log(html);
});

Затем, если вы новичок в этом, из командной строки запустите:

node server.js

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

Теперь давайте воспользуемся Cheerio, чтобы выбрать нужные элементы. Поскольку cheerio похож на jQuery на бэкэнде, соглашение состоит в том, чтобы загружать HTML, с которым вы работаете, в $, например:

var $ = cheerio.load(html);

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

console.log($("h1").text());

Ваш результат должен быть примерно таким:

Jared Nielsen
nielsenjared

Прохладный. А теперь давайте углубимся в график. Наша цель - очистить атрибут заливки, связанный с каждым элементом <rect>. Попробуйте следующее: измените «h1» на «rect», а .text () на .attr ():

console.log($("rect").attr());

У вас должно получиться примерно следующее:

{ class: 'day',
  width: '10',
  height: '10',
  x: '13',
  y: '0',
  fill: '#ebedf0',
  'data-count': '0',
  'data-date': '2017-02-19' }

Для меня на момент написания этой статьи это первый прямоугольник на моем графике. Замечательно. У нас получился один прямоугольник. Но мы хотим, чтобы их было 365 или около того. Поскольку мы загрузили Cheerio в селектор $, у нас есть доступ ко всем его встроенным методам. Мы хотим использовать .each ()

Если мы прочитаем фантастическое руководство, мы найдем такой пример:

const fruits = [];
$('li').each(function(i, elem) {
  fruits[i] = $(this).text();
});
fruits.join(', ');
//=> Apple, Orange, Pear

Измените свое приложение так:

$('rect').each(function(i, elem){
    console.log($(this).attr());
  });

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

может сбивать с толку новичков. В этом примере это примерно так:

$('rect').each(function(i, elem){
    console.log($(elem).attr());
  });

Итак, теперь мы очищаем атрибуты прямоугольника за год. Однако нам все еще нужна только заливка. Легкий. Просто передайте методу .attr конкретное свойство, которое вы хотите очистить:

$('rect').each(function(i, elem){
    console.log($(elem).attr('fill'));
  });

Теперь нам нужно что-то сделать с этими данными. Если наша цель - играть каждую неделю как «аккорд», тогда мы хотим, чтобы наш набор данных отражал недельную структуру графика. Есть несколько способов подойти к этому. Мы видели в документации выше объявление массива фруктов, в который каждый элемент <li> добавлялся по индексу. Мы могли бы воспользоваться этим подходом, а затем разделить массив на подмассивы для каждых семи элементов. Или мы могли бы копировать данные прямо в объект по неделям.

Если вы помните, наша архитектура элемента HTML выглядит так:

<svg>
    <g>
        <g>
            <rect>
        </g>
        <g>
            <rect>
        </g>
        … etc.
    </g>
    <g>
    … etc.
</svg>

Мы можем связать метод .children () и метод .eq (), чтобы получить атрибуты элементов <rect>, вложенных в элементы <g>, и добавить их по неделям в объект, например:

var url = "https://github.com/nielsenjared";
request(url, function(error, response, html) {
    var $ = cheerio.load(html);
    var data = {};
$("g g").each(function(i, element) {
      data['w' + i] = [
        $(element).children().eq(0).attr("fill"),
        $(element).children().eq(1).attr("fill"),
        $(element).children().eq(2).attr("fill"),
        $(element).children().eq(3).attr("fill"),
        $(element).children().eq(4).attr("fill"),
        $(element).children().eq(5).attr("fill"),
        $(element).children().eq(6).attr("fill")
      ]
    });
    console.log(data);
  })

Здесь мы очищаем дочерние элементы каждого вложенного <g>, где индекс равен определенному значению. Мы можем провести дальнейший рефакторинг следующим образом:

$("g g").each(function(i, element) {
      data['w' + i] = [];
      for (var j = 0; j < $(element).children().length; j++){
        data['w' + i].push($(element).children().eq(j).attr("fill"));
      }
});

Теперь мы превращаем данный пользовательский график GitHub в объект со свойствами, содержащими шестнадцатеричные значения активности за неделю.

Следующий шаг с server.js - обернуть наш парсинг в запрос Express .get() и вернуть JSON.

var cheerio = require("cheerio");
var request = require("request");
var express = require("express");
var path = require("path");
var app = express();
var PORT = process.env.PORT || 3000;
app.use(express.static('public'));
app.get("/", function(req, res) {
  res.sendFile(path.join(__dirname, "index.html"));
});
app.get("/scrape/:username", function(req, res) {
  var username = req.params.username;
  var url = "https://github.com/" + username;
  request(url, function(error, response, html) {
    var data = {};
    // var $ = cheerio.load(html);
    try {
      var $ = cheerio.load(html)
    } catch (e) {
      console.log("cheerio err"); // TODO handle error
    }
    $("g g").each(function(i, element) {
      data['w' + i] = [];
      for (var j = 0; j < $(element).children().length; j++){
        data['w' + i].push($(element).children().eq(j).attr("fill"));
      }
    });
    return res.json(data);
  })
});//end app.get("/scrape")
app.listen(PORT, function() {
  console.log("App listening on PORT " + PORT);
});

Перейдите на localhost и убедитесь, что ваш парсер работает и возвращает клиенту JSON.

Убейте сервер, поскольку он нам не понадобится, пока мы играем с Tone.js.

Клиентская сторона: сонификация данных с помощью Tone.js

Чтобы использовать Tone.js на стороне клиента, вам необходимо загрузить и добавить библиотеку в свое приложение. Загрузите и добавьте этот файл в каталог вашего проекта.

Создайте index.html файл с шаблонным HTML и добавьте Tone.js в <head>.

<script src="./Tone.min.js"></script>

Если мы RTFM, мы увидим, насколько легко играть тон. Добавьте следующее в <body> вашего index.html и откройте его в браузере:

<script>
//create a synth and connect it to the master output (your speakers)
  var synth = new Tone.Synth().toMaster()
  //play a middle 'C' for the duration of an 8th note
  synth.triggerAttackRelease('C4', '8n')
</script>

Но если мы хотим сыграть сразу несколько нот или тонов, нам необходимо использовать Polysynth. Опять же, фантастическое руководство дает нам пример:

//a polysynth composed of 6 Voices of Synth
var synth = new Tone.PolySynth(6, Tone.Synth).toMaster();
//set the attributes using the set interface
synth.set("detune", -1200);
//play a chord
synth.triggerAttackRelease(["C4", "E4", "A4"], "4n");

Как мы видим, triggerAttackRelease принимает не одну ноту, а массив нот, которые играет как аккорд. Добавленная цифра 4 обозначает октаву. Напомним, что мы планируем сыграть пентатонический аккорд в тональности G (GABDE, или do re mi so la). А теперь давайте построим тестовый аккорд.

//a polysynth composed of 6 Voices of Synth
var synth = new Tone.PolySynth(7, Tone.Synth).toMaster();
//set the attributes using the set interface
synth.set("detune", -1200);
//play a chord
synth.triggerAttackRelease(["G6", "A6", "B6", "D6", "E6"], "4n");

Мы также можем передать это как переменную

var chord = ["G6", "A6", "B6", "D6", "E6"]
var synth = new Tone.PolySynth(7, Tone.Synth).toMaster();
synth.set("detune", -1200);
synth.triggerAttackRelease(chord, "4n");

Чтобы сыграть несколько аккордов, нам нужно использовать Tone.Part. В руководстве есть этот фрагмент кода…

var part = new Tone.Part(function(time, note){
	//the notes given as the second element in the array
	//will be passed in as the second argument
	synth.triggerAttackRelease(note, "8n", time);
}, [[0, "C2"], ["0:2", "C3"], ["0:3:2", "G2"]]);

… Но здесь это менее чем фантастично. Если мы поместим приведенный выше код в наш index.html, мы ничего не получим взамен. Если мы посмотрим на Пример PolySynth, мы увидим Tone.PolySynth в действии. Веселье! Но если мы посмотрим на исходный код, есть много дополнительных настроек, которые могут запутать функциональность. Итак… давайте посмотрим следующий пример. "Прыжок!"

var synth = new Tone.PolySynth(3, Tone.Synth, {
	"oscillator" : {
		"type" : "fatsawtooth",
		"count" : 3,
		"spread" : 30
	},
	"envelope": {
		"attack": 0.01,
		"decay": 0.1,
		"sustain": 0.5,
		"release": 0.4,
		"attackCurve" : "exponential"
	},
}).toMaster();
  var part = new Tone.Part(function(time, note){
		synth.triggerAttackRelease(note.noteName, note.duration, time, note.velocity);
	}, [
		{
			"time": "192i",
			"noteName": "G4",
			"velocity": 0.8110236220472441,
			"duration": "104i"
		},
		{
			"time": "192i",
			"noteName": "B4",
			"velocity": 0.7874015748031497,
			"duration": "104i"
		},
		{
			"time": "192i",
			"noteName": "D5",
			"velocity": 0.8031496062992126,
			"duration": "104i"
		},
		{
			"time": "480i",
			"noteName": "G4",
			"velocity": 0.7559055118110236,
			"duration": "104i"
		},
		{
			"time": "480i",
			"noteName": "C5",
			"velocity": 0.6850393700787402,
			"duration": "104i"
		},
		{
			"time": "480i",
			"noteName": "E5",
			"velocity": 0.6771653543307087,
			"duration": "104i"
		},
		{
			"time": "768i",
			"noteName": "F4",
			"velocity": 0.8661417322834646,
			"duration": "104i"
		},
		{
			"time": "768i",
			"noteName": "A4",
			"velocity": 0.8346456692913385,
			"duration": "104i"
		},
		{
			"time": "768i",
			"noteName": "C5",
			"velocity": 0.8188976377952756,
			"duration": "104i"
		},
		{
			"time": "1056i",
			"noteName": "F4",
			"velocity": 0.7007874015748031,
			"duration": "104i"
		},
		{
			"time": "1056i",
			"noteName": "A4",
			"velocity": 0.6850393700787402,
			"duration": "104i"
		},
		{
			"time": "1056i",
			"noteName": "C5",
			"velocity": 0.6614173228346457,
			"duration": "104i"
		},
		{
			"time": "1248i",
			"noteName": "G4",
			"velocity": 0.6771653543307087,
			"duration": "104i"
		},
		{
			"time": "1248i",
			"noteName": "B4",
			"velocity": 0.6771653543307087,
			"duration": "104i"
		},
		{
			"time": "1248i",
			"noteName": "D5",
			"velocity": 0.7165354330708661,
			"duration": "104i"
		},
		{
			"time": "1440i",
			"noteName": "G4",
			"velocity": 0.8818897637795275,
			"duration": "248i"
		},
		{
			"time": "1440i",
			"noteName": "B4",
			"velocity": 0.84251968503937,
			"duration": "248i"
		},
		{
			"time": "1440i",
			"noteName": "D5",
			"velocity": 0.8818897637795275,
			"duration": "248i"
		},
		{
			"time": "1728i",
			"noteName": "G4",
			"velocity": 0.8267716535433071,
			"duration": "104i"
		},
		{
			"time": "1728i",
			"noteName": "C5",
			"velocity": 0.8031496062992126,
			"duration": "104i"
		},
		{
			"time": "1728i",
			"noteName": "E5",
			"velocity": 0.8188976377952756,
			"duration": "104i"
		},
		{
			"time": "2016i",
			"noteName": "F4",
			"velocity": 0.7086614173228346,
			"duration": "104i"
		},
		{
			"time": "2016i",
			"noteName": "A4",
			"velocity": 0.7244094488188977,
			"duration": "104i"
		},
		{
			"time": "2016i",
			"noteName": "C5",
			"velocity": 0.7007874015748031,
			"duration": "104i"
		},
		{
			"time": "2208i",
			"noteName": "C4",
			"velocity": 0.9921259842519685,
			"duration": "296i"
		},
		{
			"time": "2208i",
			"noteName": "F4",
			"velocity": 0.968503937007874,
			"duration": "200i"
		},
		{
			"time": "2208i",
			"noteName": "A4",
			"velocity": 0.9606299212598425,
			"duration": "208i"
		},
		{
			"time": "2400i",
			"noteName": "E4",
			"velocity": 0.7559055118110236,
			"duration": "104i"
		},
		{
			"time": "2400i",
			"noteName": "G4",
			"velocity": 0.7007874015748031,
			"duration": "104i"
		},
		{
			"time": "2592i",
			"noteName": "C4",
			"velocity": 0.968503937007874,
			"duration": "488i"
		},
		{
			"time": "2592i",
			"noteName": "D4",
			"velocity": 0.9448818897637795,
			"duration": "488i"
		},
		{
			"time": "2592i",
			"noteName": "G4",
			"velocity": 0.937007874015748,
			"duration": "488i"
		}
]).start(0);
part.loop = true;
part.loopEnd = "4m";
Tone.Transport.bpm.value = 132;
//pulled this out of the Interface.Button to autoplay on page load
Tone.Transport.start("+0.1");

Из этого примера мы видим, что мы инициализируем новый Tone.PolySynth synth, а затем инициализируем part как новый Tone.Part. В Tone.Part мы вызываем synth.triggerAttackRelease и передаем part массив объектов, содержащих последовательность аккордов Jump. Давайте упростим это, чтобы увидеть, что происходит:

var chords = [
  {
    "time": "192i",
    "noteName": "G4",
    "velocity": 0.8110236220472441,
    "duration": "104i"
  },
  {
    "time": "192i",
    "noteName": "B4",
    "velocity": 0.7874015748031497,
    "duration": "104i"
  },
  {
    "time": "192i",
    "noteName": "D5",
    "velocity": 0.8031496062992126,
    "duration": "104i"
  },
  {
    "time": "480i",
    "noteName": "G4",
    "velocity": 0.7559055118110236,
    "duration": "104i"
  },
  {
    "time": "480i",
    "noteName": "C5",
    "velocity": 0.6850393700787402,
    "duration": "104i"
  },
  {
    "time": "480i",
    "noteName": "E5",
    "velocity": 0.6771653543307087,
    "duration": "104i"
  },
  {
    "time": "768i",
    "noteName": "F4",
    "velocity": 0.8661417322834646,
    "duration": "104i"
  },
  {
    "time": "768i",
    "noteName": "A4",
    "velocity": 0.8346456692913385,
    "duration": "104i"
  },
  {
    "time": "768i",
    "noteName": "C5",
    "velocity": 0.8188976377952756,
    "duration": "104i"
  },
  {
    "time": "1056i",
    "noteName": "F4",
    "velocity": 0.7007874015748031,
    "duration": "104i"
  },
  {
    "time": "1056i",
    "noteName": "A4",
    "velocity": 0.6850393700787402,
    "duration": "104i"
  },
  {
    "time": "1056i",
    "noteName": "C5",
    "velocity": 0.6614173228346457,
    "duration": "104i"
  },
  {
    "time": "1248i",
    "noteName": "G4",
    "velocity": 0.6771653543307087,
    "duration": "104i"
  },
  {
    "time": "1248i",
    "noteName": "B4",
    "velocity": 0.6771653543307087,
    "duration": "104i"
  },
  {
    "time": "1248i",
    "noteName": "D5",
    "velocity": 0.7165354330708661,
    "duration": "104i"
  },
  {
    "time": "1440i",
    "noteName": "G4",
    "velocity": 0.8818897637795275,
    "duration": "248i"
  },
  {
    "time": "1440i",
    "noteName": "B4",
    "velocity": 0.84251968503937,
    "duration": "248i"
  },
  {
    "time": "1440i",
    "noteName": "D5",
    "velocity": 0.8818897637795275,
    "duration": "248i"
  },
  {
    "time": "1728i",
    "noteName": "G4",
    "velocity": 0.8267716535433071,
    "duration": "104i"
  },
  {
    "time": "1728i",
    "noteName": "C5",
    "velocity": 0.8031496062992126,
    "duration": "104i"
  },
  {
    "time": "1728i",
    "noteName": "E5",
    "velocity": 0.8188976377952756,
    "duration": "104i"
  },
  {
    "time": "2016i",
    "noteName": "F4",
    "velocity": 0.7086614173228346,
    "duration": "104i"
  },
  {
    "time": "2016i",
    "noteName": "A4",
    "velocity": 0.7244094488188977,
    "duration": "104i"
  },
  {
    "time": "2016i",
    "noteName": "C5",
    "velocity": 0.7007874015748031,
    "duration": "104i"
  },
  {
    "time": "2208i",
    "noteName": "C4",
    "velocity": 0.9921259842519685,
    "duration": "296i"
  },
  {
    "time": "2208i",
    "noteName": "F4",
    "velocity": 0.968503937007874,
    "duration": "200i"
  },
  {
    "time": "2208i",
    "noteName": "A4",
    "velocity": 0.9606299212598425,
    "duration": "208i"
  },
  {
    "time": "2400i",
    "noteName": "E4",
    "velocity": 0.7559055118110236,
    "duration": "104i"
  },
  {
    "time": "2400i",
    "noteName": "G4",
    "velocity": 0.7007874015748031,
    "duration": "104i"
    },
    {
      "time": "2592i",
      "noteName": "C4",
      "velocity": 0.968503937007874,
      "duration": "488i"
    },
    {
      "time": "2592i",
      "noteName": "D4",
      "velocity": 0.9448818897637795,
      "duration": "488i"
    },
    {
      "time": "2592i",
      "noteName": "G4",
      "velocity": 0.937007874015748,
      "duration": "488i"
    }
];
var synth = new Tone.PolySynth(3, Tone.Synth, {
  "oscillator" : {
    "type" : "fatsawtooth",
    "count" : 3,
    "spread" : 30
  },
  "envelope": {
    "attack": 0.01,
    "decay": 0.1,
    "sustain": 0.5,
    "release": 0.4,
    "attackCurve" : "exponential"
  },
}).toMaster();
var part = new Tone.Part(function(time, note){
  synth.triggerAttackRelease(note.noteName, note.duration, time, note.velocity);
}, chords).start(0);
part.loop = true;
part.loopEnd = "4m";
Tone.Transport.bpm.value = 132;
//
Tone.Transport.start("+0.1");

Я переместил наши аккорды в переменную chords, а затем передал ее Tone.Part в качестве второго аргумента после обратного вызова. Если мы обратимся к документации, мы увидим, что базовое использование выглядит так: new Tone.Part ( callback , events ) Где обратный вызов - это то, что мы хотим вызывать для каждого события, и каждое событие передается как элемент в массиве.

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

function callback(time, note){
  synth.triggerAttackRelease(note.noteName, note.duration, time, note.velocity);
}
var part = new Tone.Part(callback, chords).start(0);

Давайте еще больше упростим это и посмотрим, как передавать массив массивов, а не объект. Возвращаясь к документации, у нас есть этот пример:

var part = new Tone.Part(function(time, note){
	//the notes given as the second element in the array
	//will be passed in as the second argument
	synth.triggerAttackRelease(note, "8n", time);
}, [[0, "C2"], ["0:2", "C3"], ["0:3:2", "G2"]]);

Давайте заменим нашу переменную chords массивом, который мы видим выше.

var chords = [[0, "C2"], ["0:2", "C3"], ["0:3:2", "G2"]];

Затем измените наш обратный вызов, удалив ссылки на свойства объекта:

function callback(time, note){
  synth.triggerAttackRelease(note, "8n", time);
}

Спереди && Назад

Теперь все, что нам нужно сделать, это подключить скребок серверной части к интерфейсу. Для этого нам нужно, чтобы наш сервер работал, так что запустите его. Если вы его не используете, рекомендую nodemon: nodemon server.js

Вот несколько начальных HTML и jQuery для ускорения процесса (обратите внимание, что вам нужно будет создать каталог public и переместить в него файл Tone, чтобы Express мог обслуживать статические файлы):

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <script src="./Tone.min.js"></script>
  </head>
  <body>
    <div id="container">
      <form id="github-user-form">
        <input type="text" id="github-username" placeholder="Enter GitHub username" size="25">
        <input id="start-button" type="submit" value="Start">
      </form>
      <div id="contribution-graph"></div>
    </div>
    <script src="https://code.jquery.com/jquery-1.11.1.js"></script>
    <script>
    $("#start-button").on("click", function(event) {
      var username = $("#github-username").val().trim();
      event.preventDefault();
      $.get('/scrape/' + username, function(data){
        console.log(data);
      });
    });
    </script>
  </body>
</html>

Введите имя пользователя GitHub в поле ввода и нажмите «Отправить». Используя инструменты инспектора разработчика, вы должны увидеть в консоли объект, содержащий все очищенные нами пользовательские данные.

Давай что-нибудь сделаем с этими данными.

Объявите глобальную переменную chords и присвойте ей пустой массив.

Если вспомнить, Tone.Part ожидает получить массив массивов. Давайте добавим это в наш GET-запрос на «/ scrape»:

var chords = [];
$("#start-button").on("click", function(event) {
  var username = $("#github-username").val().trim();
  event.preventDefault();
  $.get('/scrape/' + username, function(data){
    var time = 0;
    for (key in data) {
      var chord = [];
      chord.push(time + "i");
      time+=10;
      var notes = [];
      for (var i = 1; i <= data[key].length; i++){
        if (data[key][i-1] === "#196127") {
          notes.push(data[key][i-1] = "E" + i);
        }
        else if (data[key][i-1] === "#239a3b") {
          notes.push(data[key][i-1] = "D" + i);
        }
        else if (data[key][i-1] === "#7bc96f") {
          notes.push(data[key][i-1] = "B" + i);
        }
        else if (data[key][i-1] === "#c6e48b") {
          notes.push(data[key][i-1] = "A" + i);
        }
        else {
          notes.push(data[key][i-1] = "G" + i);
        }
      }
      chord.push(notes);
      chords.push(chord);
    }
  });
  console.log(chords);
});

Добавьте console.log () в jQuery при «щелчке» слушателя, чтобы мы могли видеть, правильно ли строится наш массив.

Ошибок нет? Холодные бобы.

Давайте добавим наш синтезатор.

var synth = new Tone.PolySynth(7, Tone.Synth, {
    "volume" : -8,
    "oscillator" : {
      "partials" : [1, 2, 1],
    },
    "portamento" : 0.05
  }).toMaster()

И наша часть…

var synthPart = new Tone.Part(function(time, chord){
      synth.triggerAttackRelease(chord, '16n', time);
    }, chords).start("0");
    synthPart.loop = false;
    synthPart.humanize = false;
    Tone.Transport.bpm.value = 10;
    Tone.Transport.start("+0.1");

Теперь нам нужно добавить новый класс Tone.js, Tone.Draw. Это позволит нам синхронизировать визуальные события со звуковыми событиями.

Внутри synthPart добавьте следующее:

synthPart = new Tone.Part(function(time, chord){
      synth.triggerAttackRelease(chord, '16n', time);
      Tone.Draw.schedule(function(){
        $('#contribution-graph').text(chord);
      }, time);
    }, chords).start("0");

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

Последняя часть - добавить немного стиля. В каталоге public создайте новую таблицу стилей style.css и свяжите ее с index.html в <head>:

<link rel="stylesheet" type="text/css" href="./style.css">

Добавьте в style.css следующее:

.G1, .G2, .G3, .G4, .G5, .G6, .G7 {
  background-color: #ebedf0;
	height: 10px;
	margin: 6%;
}
.A1, .A2, .A3, .A4, .A5, .A6, .A7 {
  background-color: #c6e48b;
	height: 10px;
	margin: 6%;
}
.B1, .B2, .B3, .B4, .B5, .B6, .B7 {
  background-color: #7bc96f;
	height: 10px;
	margin: 6%;
}
.D1, .D2, .D3, .D4, .D5, .D6, .D7 {
  background-color: #239a3b;
	height: 10px;
	margin: 6%;
}
.E1, .E2, .E3, .E4, .E5, .E6, .E7 {
  background-color: #196127;
	height: 10px;
	margin: 6%;
}
.week {
	position: relative;
	float: left;
	width: 10px;
	/*width: 1.75%;*/
}

Наконец, внутри Tone.Draw добавьте следующее:

var sun = $("<div>&nbsp;</div>").attr("class", chord[0]);
    var mon = $("<div>&nbsp;</div>").attr("class", chord[1]);
    var tue = $("<div>&nbsp;</div>").attr("class", chord[2]);
    var wed = $("<div>&nbsp;</div>").attr("class", chord[3]);
    var thu = $("<div>&nbsp;</div>").attr("class", chord[4]);
    var fri = $("<div>&nbsp;</div>").attr("class", chord[5]);
    var sat = $("<div>&nbsp;</div>").attr("class", chord[6]);
    var week = $("<div>").addClass("animated pulse week").append(sun, mon, tue, wed, thu, fri, sat);
    $("#contribution-graph").append(week);

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