Pull to refresh

Пианино на Javascript в 12 строк

Хотя время 30-строчников определённо подходит к концу, уже появились примеры «Hello, world» и советы пойти в какое-нибудь другое место, неделя ещё не закончилась, и я хочу опубликовать свой вариант пианина на Javascript в ответ на творчество oshibka404 Его версия занимает целых 24 строки, то есть на медленных соединениях она будет слишком долго загружаться. Я попытался применить примерно те же идеи, но упростить код.

Хотя количество строк кода — не особо полезная мера, но я всё же думаю, что моя версия короче. Она умещается в 1134 символа как есть, а минификатор jscompress.com ужимает её до 875, а версию oshibka404 с наскоку даже в 1024 символа не запихнёшь. Кроме того, его версию JSFiddle в режиме «Tidy Up» разбивает на 46 строк, а мою — на 24.

Внимание. В этой статье я не буду объяснять теорию работы фортепиано, кто с ней не знаком, рекомендую прочесть оригинальную статью и посмотреть важную картинку. Кроме того, нужно уже понимать основы работы с HTML5 Web Audio API.

И да, ещё хочу заметить, что я сам не музыкант, да и на Javascript писать толком не умею.


Код и пояснения — под катом.

Где же код?


Вот тут
var aud = window.AudioContext ? new AudioContext() : new webkitAudioContext(), make = function(scheme, num, LWR, result) {
	var hz = Math.pow(2, (num - 48) / 12) * 440,
	code = '<div style="position: absolute; width: Wpx; height: Hpx; left: Lpx; background-color: C; border: 1px solid black; z-index: Z" id="F"></div>',
	r = function(s, re, subst) { return s.replace(re, subst); };
	result += (scheme[0] == 'W' && r(r(r(r(r(r(code, /W/, '20'), /H/, '300'), /L/, '' + LWR), /C/, 'white'), /F/, '' + hz), /Z/, 1) ||
		r(r(r(r(r(r(code, /W/, '10'), /H/, '200'), /L/, (LWR - 5) + ''), /C/, 'black'), /F/, '' + hz), /Z/, 2));
	return scheme.length == 1 && result || make(scheme.substr(1), num + 1, (scheme[0] == 'W' && LWR + 20 || LWR), result);
}; document.body.innerHTML = make('WBW!!!!!!!W'.replace(/!/g, 'WBWBWWBWBWBW'), 0, 1, "");
document.body.addEventListener('click', function(evt) {
	if (evt.target.id) { var osc = aud.createOscillator(); osc.frequency.value = evt.target.id * 1; osc.type = 'square'; osc.connect(aud.destination); osc.start(0);
	setTimeout(function() { osc.stop(0); osc.disconnect(aud.destination); }, 500); }
});


Код протестирован в Chromium 30 и Firefox 25.

Ещё можно его посмотреть на JSFiddle (или сразу поиграть).

Как оно работает?


В основе лежит идея о том, что фортепиано настраивается по ноте ля первой октавы (спасибо DrSmile за наводку). Частота звука у неё 400 герц. Частота звука любой другой ноты — 2n / 12 ∗ 440, где n — расстояние от нужной ноты до ля первой октавы в полутонах. Первая клавиша — ля субконтроктавы, отстоит от неё на 48 полутонов, как нетрудно убедиться, взглянув на изображение фортепианной клавиатуры с разделением на октавы (ссылка выше).

Изображение клавиатуры на экране представляет собой набор div'ов, стилизованных CSS. Они генерируются в виде HTML-кода функцией make(scheme, num, LWR, result), где num — номер клавиши (первая клавиша имеет номер 0), result — аккумулятор для уже сгенерированного HTML-кода, а LWR (Last-White-Right) — координата X каймы (border) последней белой клавиши — следующая белая клавиша должна примыкать непосредственно к ней, чтобы её кайма слева накладывалась на правую кайму предыдущей клавишы, а чёрная клавиша должна заезжать половиной ширины на предыдущую белую. Мы начинаем с 1, чтобы первая клавиша располагалась на отрезке от 1 до 20 по оси X, тогда её кайма будет на 0 и на 21. scheme — строка из букв W и B, которая описывает вид клавиатуры (W — белая клавиша, B — чёрная). Такой подход позволяет нам не сильно затрудняться из-за отсутствия нот си-диез и ми-диез, а также неполноты субконтроктавы и пятой октавы.

Применяются захардкоженные значения: высота белой клавиши — 300, ширина — 20 (не считая каймы), z-index = 1 (чтобы все белые были под чёрными). Высота чёрной клавишы (опять-таки не считая каймы в 1 пиксель) — 200, ширина — 10, то есть половина ширины — 5, z-index = 2. Эти те же значения, что были в оригинальной версии.

Частоту каждой клавиши мы записываем (в виде строки) ей в ID. Для краткости применяется конверсия из числа в строку при помощи '' + число, а из строки в число — при помощи строка * 1.

Функция r служит только для того, чтобы не писать String.replace.

Программисты на других языках могут не вполне понимать принцип работы операторов && и ||. && возвращает свой первый операнд, если его значение — ложь (булево false, 0, пустая строка, null, undefined), иначе — второй операнд. || возвращает первый операнд, если его значение — истина (всё, кроме лжи), иначе — второй операнд.

Ну, а далее уже дело техники. Когда пользователь кликает на любую клавишу (а это любой элемент с непустым ID), мы превращаем ID в число, и это есть частота звука, который нам надо воспроизвести.

Я надеюсь, что с этими пояснениями прочитать сам код будет нетрудно, там всего 12 строк.

Некоторые примечания


Код был бы короче, если бы вместо addEventListener приделать просто каждому div'у свойство onclick=«play(частота)» (я не знаю, как правильно сделать прямые кавычки). Не надо было бы хранить частоту в ID. Но тогда это не будет работать в JSFiddle.

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

Изначально я попытался рисовать всё пианино на canvas'е. У меня тоже получилось 12 строк, но большая часть из них представляла собой расчётные формулы, чтобы их объяснить человеку с пятничным настроением, понадобилась бы гораздо более длинная статья с картинками. Впрочем, если кому-то интересно…
Бонус
(function(){var dx, dy, img, WKW = 22, TW = WKW * 52, TH = 300, BKH = 200, BKW = 12, BKD = WKW - (BKW / 2) - 1, fl = Math.floor, cnv = document.createElement('canvas'), ctx = cnv.getContext('2d'), 
key = function(x, y) { var A = fl(x / WKW), R = A % 7; return [fl(A / 7), R, y < BKH && (A && R != 2 && R != 5 && x < (A - 1) * WKW + BKD + BKW && -1 || A != 51 && R != 1 && R != 4 && x > A * WKW + BKD && 1) || 0] };
cnv.width = TW; cnv.height = TH; img = ctx.getImageData(0, 0, TW, TH);
for (var i = 0; i < TW * TH * 4; i++) {
	var x = fl(i % (TW * 4) / 4), y = fl(i / TW / 4); img.data[i] = !(i % 4 != 3 && (!y || y == TH - 1 || !x || x % WKW == WKW - 1 || key(x, y)[2])) && 255 || 0;
}
ctx.putImageData(img, 0, 0); var aud = window.AudioContext ? new AudioContext() : new webkitAudioContext(), playfunc = function(evt) {
	var D = key(evt.clientX - dx, evt.clientY - dy), Nd = D[1] * 2 - (D[1] >= 5 && 2 || D[1] >= 2 && 1 || 0) + D[2] - 48,
	F = Math.pow(2, Nd / 12 + D[0]) * 440,
	osc = aud.createOscillator(); osc.frequency.value = F; osc.type = 'square'; osc.connect(aud.destination); osc.start(0);
	setTimeout(function() { osc.stop(0); osc.disconnect(aud.destination); }, 1000 / 2);
}; cnv.addEventListener('click', playfunc); document.body.appendChild(cnv); dx = cnv.offsetLeft; dy = cnv.offsetTop;})();

Примечание: WKW — White Key Width, TW — Total Width, TH — Total Height, BKH — Black Key Height, BKD — Black Key Distance (отступ от начала белой клавиши о следующей чёрной, например, от соль до соль-диез).
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.