Пианино в 24 строки на Javascript: если играть, то музыку

в 3:03, , рубрики: javascript, JS, web audio api, музыка, ненормальное программирование, метки: ,

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

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

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

Итак, начнем.

Клавиатура классического фортепиано состоит из 88 клавиш, покрывающих диапазон от A0 (Ля суб-контр-октавы, частота звучания 27.5 Гц) до C8 (До пятой октавы, частота 4186 Гц). Каждая октава на клавиатуре состоит из двенадцати нот:
До, До-диез, Ре, Ре-диез, Ми, Фа, Фа-диез, Соль, Соль-диез, Ля, Ля-диез/Си-бемоль, Си. Жирным выделены клавиши верхнего ряда, они на клавиатуре обычно бывают черного цвета.

Собственно, вот так выглядит одна октава:

image

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

Nx = N1 × 2x-1, где:

  • N – название ноты;
  • x — номер октавы (от 0 до 8);
  • Nx, соответственно, частота звука, соответствующая ноте N октавы x;

В формуле фигурирует N1 вместо N0 лишь потому, что часть нот суб-контр-октавы (N0) имеет частоту звучания ниже порога слышимости человеческим ухом (< 20 Hz).

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

C: 32.703,
С#: 34.648,
D: 36.708,
D#: 38.891,
E: 41.203,
F: 43.654,
F#: 46.249,
G: 48.999,
G#: 51.913,
A: 55,
A#: 58.27,
B: 61.735

Основываясь на этом, пишем функцию, принимающую в качестве аргумента строку с именем клавиши в виде "A4" или "C5#", и возвращающую частоту её звучания:

function play(key) {
	var controctave = {
			'C': 32.703,
			'С#': 34.648,
			'D': 36.708,
			'D#': 38.891,
			'E': 41.203,
			'F': 43.654,
			'F#': 46.249,
			'G': 48.999,
			'G#': 51.913,
			'A': 55,
			'A#': 58.27,
			'B': 61.735,
		},
		note = key[0].toUpperCase(),
		octave = parseInt(key[1]),
		sharp = key[2] == '#' ? true : false;
	if (sharp) {
		return controctave[note + '#'] * Math.pow(2, octave-1);
	} else {
		return controctave[note] * Math.pow(2, octave-1);
	}
}

Ах, да, мы же пишем не красиво, а коротко. Немного подсократим:

function play(key) {
	var controctave = { 'C': 32.703, 'С#': 34.648, 'D': 36.708, 'D#': 38.891, 'E': 41.203, 'F': 43.654, 'F#': 46.249, 'G': 48.999, 'G#': 51.913, 'A': 55, 'A#': 58.27, 'B': 61.735};
	freq = key[2] == '#' ? controctave[key[0].toUpperCase() + '#'] * Math.pow(2, (key[1]|0) - 1) : controctave[key[0].toUpperCase()] * Math.pow(2, (key[1]|0) - 1);
	return freq; }

Уже использовано четыре строчки кода.

Давайте нарисуем клавиатуру

88 клавиш клавиатуры начинаются с ноты Ля (A0).
Соответственно, цикл будет такой: в цикле рисуем по 12 клавиш, и каждую вторую, четвертую, седьмую, девятую и одиннадцатую делаем черной. Каждой клавише присвоим id, соответствующей ноте, которую она должна воспроизводить при нажатии.

В общем, так:

var width = 1000;
var deck = document.createElement('div'), 
	octave = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'], 
	id = "", 
	keynumber = 0,
	whitekeys = 0,
	keys = [];

deck.style.width = width;

parent:
for (var i = 0; i < 8; i++) {
	for (var j = 0; j < 12; j++) {
		keynumber = (i * 12) + j;
		if (keynumber >= 88) break parent;
		keys[keynumber] = document.createElement('div');
		keys[keynumber].style.border = "1px solid black";
		keys[keynumber].style.position = "absolute";
		id = (octave[j][1] == '#') ? octave[j] + i + 's' : octave[j] + i;
		keys[keynumber].id = id;

		switch(j%12) {
			case 1: 
			case 3: 
			case 6: 
			case 8:
			case 10:
				keys[keynumber].style.backgroundColor = 'black';
				keys[keynumber].style.left = ((width / 50 * whitekeys) - (width / 200)) + 'px';
				keys[keynumber].style.width = width/100 + "px";
				keys[keynumber].style.height = "200px";
				keys[keynumber].style.zIndex = 10;
				break;
			default:
				keys[keynumber].style.backgroundColor = 'white';
				keys[keynumber].style.left = (width / 50 * whitekeys) + 'px';
				keys[keynumber].style.width = width/50 + "px";
				keys[keynumber].style.height = "300px";
				whitekeys++;

		}

		deck.appendChild(keys[keynumber]);

	}
}

document.body.appendChild(deck);

И вновь превратим нормальный код в нечитабельное говнище применим небольшую оптимизацию.

var width = 1000, octave = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'], id = "", div, whitekeys=0, keys = [];
parent: for (var i = 0; i < 8; i++) {
	for (var j = 0; j < 12; j++) {
		if ((i * 12) + j >= 88) break parent;
		div = document.createElement('div');
		div.id = (octave[j][1] == '#') ? octave[j][0] + ((((i * 12) + j + 9) / 12)|0) + 's' : octave[j] + ((((i * 12) + j + 9) / 12)|0);
		if (j % 12 == 1 || j % 12 == 4 || j % 12 == 6 || j % 12 == 9 || j % 12 == 11) {
			div.setAttribute('style', 'border:1px solid black; position:absolute; background-color:black; left:' + ((width / 50 * whitekeys) - (width / 200)) + 'px; width:' + width/100 + 'px; height: 200px; z-index:1;');}
		else {
			div.setAttribute('style', 'border:1px solid black; position:absolute; background-color:white; left:' + (width / 50 * whitekeys) + 'px; width:' + width/50 + 'px; height:300px;');
			whitekeys++;
		}
		document.body.appendChild(div);}}

Мы израсходовали ещё 13 строк.

Научим пианино издавать звуки

Для этого нам понадобится Web Audio API, который на сей момент поддерживается только Webkit-based браузерами и Firefox.

Добавим в строку объявления глобальных переменных создание аудиоконтекста:

context = window.AudioContext ? new AudioContext() : new webkitAudioContext();

добавим обработчик нажатий на клавиши:

document.body.addEventListener('click', play);

а саму функцию play изменим следующим образом:

function play(e) {
	var controctave = { 'C': 32.703, 'Cs': 34.648, 'D': 36.708, 'Ds': 38.891, 'E': 41.203, 'F': 43.654, 'Fs': 46.249, 'G': 48.999, 'Gs': 51.913, 'A': 55, 'As': 58.27, 'B': 61.735}, osc = context.createOscillator();
	osc.frequency.value = e.target.id[2] == 's' ? controctave[e.target.id[0] + 's'] * Math.pow(2, (e.target.id[1]|0) - 1) : controctave[e.target.id[0]] * Math.pow(2, (e.target.id[1]|0) - 1);
	osc.type = "square";
	osc.connect(context.destination);
	osc.start(0);
	setTimeout(function() {
	    osc.stop(0);
	    osc.disconnect(context.destination);
	}, 1000 / 2);}

Здесь мы создали осциллятор: osc = context.createOscillator();, установили ему необходимую частоту звучания: osc.frequency.value = e.target.id[2] == 's' ? controctave[e.target.id[0] + 's'] * Math.pow(2, (e.target.id[1]|0) - 1) : controctave[e.target.id[0]] * Math.pow(2, (e.target.id[1]|0) - 1); (ну, мы же не следим за чистотой и опрятностью кода, не так ли?), установили форму сигнала: osc.type = "square"; (по умолчанию был синусоидальный) соединили его с устройством вывода звука: osc.connect(context.destination);, и дали команду начать воспроизведение: osc.start(0);. После этого нам необходимо заставить клавишу замолчать через некоторое время (500мс), а то она так и будет противно пищать. Для этого используем osc.stop(0), завёрнутый в интервал. Обязательный элемент — osc.disconnect(context.destination); — отключаем осциллятор от устройства вывода.

Резюмируем: у нас получился вот такой нехитрый код:

var width = 1000, octave = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'], id = "", div, whitekeys=0, keys = [],context = window.AudioContext ? new AudioContext() : new webkitAudioContext();
parent: for (var i = 0; i < 8; i++) {
	for (var j = 0; j < 12; j++) {
		if ((i * 12) + j >= 88) break parent;
		div = document.createElement('div');
		div.id = (octave[j][1] == '#') ? octave[j][0] + ((((i * 12) + j + 9) / 12)|0) + 's' : octave[j] + ((((i * 12) + j + 9) / 12)|0);
		if (j % 12 == 1 || j % 12 == 4 || j % 12 == 6 || j % 12 == 9 || j % 12 == 11) {
			div.setAttribute('style', 'border:1px solid black; position:absolute; background-color:black; left:' + ((width / 50 * whitekeys) - (width / 200)) + 'px; width:' + width/100 + 'px; height: 200px; z-index:1;');}
		else {
			div.setAttribute('style', 'border:1px solid black; position:absolute; background-color:white; left:' + (width / 50 * whitekeys) + 'px; width:' + width/50 + 'px; height:300px;');
			whitekeys++;
		}
		document.body.appendChild(div);}}
document.body.addEventListener('click', play);
function play(e) {
	var controctave = { 'C': 32.703, 'Cs': 34.648, 'D': 36.708, 'Ds': 38.891, 'E': 41.203, 'F': 43.654, 'Fs': 46.249, 'G': 48.999, 'Gs': 51.913, 'A': 55, 'As': 58.27, 'B': 61.735}, osc = context.createOscillator();
	osc.frequency.value = e.target.id[2] == 's' ? controctave[e.target.id[0] + 's'] * Math.pow(2, (e.target.id[1]|0) - 1) : controctave[e.target.id[0]] * Math.pow(2, (e.target.id[1]|0) - 1);
	osc.type = "square";
	osc.connect(context.destination);
	osc.start(0);
	setTimeout(function() {
	    osc.stop(0);
	    osc.disconnect(context.destination);
	}, 1000 / 2);}

В заключение хочу сказать, что теперь меня надо называть Страдивари XXI века Web Audio API — штука очень классная и интересная. Почитать про него можно, естественно, на MDN, могу посоветовать милый туториал на HTML5Rocks и ещё один забавный эксперимент.

А пианино вышло жутко примитивное, но экспериментом я всё равно доволен. Надеюсь, вам тоже было интересно.

Поиграть

Посмотреть код

P.S. динамики макбука, например, отказываются издавать слышимые звуки вплоть до малой октавы (т.е. до 130 Гц), что неудивительно. В общем, не удивляйтесь, если левая часть клавиатуры будто бы вообще не звучит.

Автор: oshibka404

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js