Spec-Zone.ru › Web APIs

Пример и учебник: Простой синтезатор с клавиатурой

В этой статье представлен код и демонстрация работы виртуальной клавиатуры, которой можно управлять с помощью мыши. Клавиатура позволяет переключаться между стандартными формами волн, а также одной пользовательской формой волны, и вы можете управлять основным усилением с помощью ползунка громкости под клавиатурой. В этом примере используются следующие интерфейсы API веб-браузера: AudioContext, OscillatorNode, PeriodicWave и GainNode.

Так как OscillatorNode основан на AudioScheduledSourceNode, это в какой-то мере также пример для него.

Виртуальная клавиатура

HTML

Для отображения нашей виртуальной клавиатуры есть три основных компонента. Первый — это сама музыкальная клавиатура. Мы рисуем её в паре вложенных элементов <div>, чтобы можно было горизонтально прокручивать клавиатуру, если все клавиши не помещаются на экране, без их обрезания.

Клавиатура

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

<div class="container">
  <div class="keyboard"></div>
</div>

Элемент <div> с именем "container" — это область прокрутки, которая позволяет горизонтально прокручивать клавиатуру, если она слишком широкая для доступного пространства. Сами клавиши будут вставлены в блок с классом "keyboard".

Панель настроек

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

Управление громкостью

Сначала мы создаём элемент <div> для размещения панели настроек, чтобы её можно было стилизовать по мере необходимости. Затем мы создаём область, которая будет представлена слева на панели, и размещаем метку и элемент <input> типа "range". Элемент типа range обычно представляется в виде ползунка; мы настраиваем его так, чтобы он допускал любые значения от 0,0 до 1,0 с шагом 0,01 на каждой позиции.

<div class="settingsBar">
  <div class="left">
    <span>Volume: </span>
    <input
      type="range"
      min="0.0"
      max="1.0"
      step="0.01"
      value="0.5"
      list="volumes"
      name="volume" />
    <datalist id="volumes">
      <option value="0.0" label="Mute"></option>
      <option value="1.0" label="100%"></option>
    </datalist>
  </div>
</div>

Мы задаём значение по умолчанию 0,5 и предоставляем элемент <datalist>, который связан с элементом range с помощью атрибута list для поиска списка опций, ID которого совпадает; в этом случае набор данных называется "volumes". Это позволяет нам предоставить набор общих значений и специальных строк, которые браузер может дополнительно отобразить каким-либо способом; мы предоставляем имена значений 0,0 («Выкл.») и 1,0 («100%»).

Выбор формы волны

Справа от панели настроек мы размещаем метку и элемент <select> с именем "waveform", опции которого соответствуют доступным формам волн.

  <div class="right">
    <span>Current waveform: </span>
    <select name="waveform">
      <option value="sine">Sine</option>
      <option value="square" selected>Square</option>
      <option value="sawtooth">Sawtooth</option>
      <option value="triangle">Triangle</option>
      <option value="custom">Custom</option>
    </select>
  </div>
</div>

CSS

.container {
  overflow-x: scroll;
  overflow-y: hidden;
  width: 660px;
  height: 110px;
  white-space: nowrap;
  margin: 10px;
}

.keyboard {
  width: auto;
  padding: 0;
  margin: 0;
}

.key {
  cursor: pointer;
  font:
    16px "Open Sans",
    "Lucida Grande",
    "Arial",
    sans-serif;
  border: 1px solid black;
  border-radius: 5px;
  width: 20px;
  height: 80px;
  text-align: center;
  box-shadow: 2px 2px darkgray;
  display: inline-block;
  position: relative;
  margin-right: 3px;
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}

.key div {
  position: absolute;
  bottom: 0;
  text-align: center;
  width: 100%;
  pointer-events: none;
}

.key div sub {
  font-size: 10px;
  pointer-events: none;
}

.key:hover {
  background-color: #eef;
}

.key:active,
.active {
  background-color: #000;
  color: #fff;
}

.octave {
  display: inline-block;
  padding: 0 6px 0 0;
}

.settingsBar {
  padding-top: 8px;
  font:
    14px "Open Sans",
    "Lucida Grande",
    "Arial",
    sans-serif;
  position: relative;
  vertical-align: middle;
  width: 100%;
  height: 30px;
}

.left {
  width: 50%;
  position: absolute;
  left: 0;
  display: table-cell;
  vertical-align: middle;
}

.left span,
.left input {
  vertical-align: middle;
}

.right {
  width: 50%;
  position: absolute;
  right: 0;
  display: table-cell;
  vertical-align: middle;
}

.right span {
  vertical-align: middle;
}

.right input {
  vertical-align: baseline;
}

JavaScript

Код JavaScript начинается с инициализации ряда переменных.

const audioContext = new AudioContext();
const oscList = [];
let mainGainNode = null;
  1. audioContext создаётся как экземпляр AudioContext.
  2. oscList настраивается для хранения списка всех в данный момент воспроизводимых генераторов колебаний. Он начинает пустым, так как пока генераторы не воспроизводятся.
  3. mainGainNode устанавливается в значение null; во время процесса настройки он будет настроен на содержание узла GainNode, к которому будут подключены и через который будут воспроизводиться все воспроизводимые генераторы колебаний, чтобы разрешить управление общей громкостью с помощью одного ползунка.
const keyboard = document.querySelector(".keyboard");
const wavePicker = document.querySelector("select[name='waveform']");
const volumeControl = document.querySelector("input[name='volume']");

Получаются ссылки на элементы, к которым нам нужно будет получить доступ:

  • keyboard — контейнерный элемент, в который будут помещены клавиши.
  • wavePicker — элемент <select>, используемый для выбора формы волны для нот.
  • volumeControl — элемент <input> (типа "range") для управления основной громкостью аудио.
let noteFreq = null;
let customWaveform = null;
let sineTerms = null;
let cosineTerms = null;

Наконец, создаются глобальные переменные, которые будут использоваться при построении форм волн:

  • noteFreq будет массивом массивов; каждый массив представляет собой октаву, каждый из которых содержит по одной записи для каждой ноты в этой октаве. Значение каждого — частота тона ноты в Герцах.
  • customWaveform будет настроен как PeriodicWave, описывающий форму волны, используемую, когда пользователь выбирает «Пользовательская» в элементе управления выбором формы волны.
  • sineTerms и cosineTerms будут использоваться для хранения данных для генерации формы волны; каждый будет содержать массив, сгенерированный, когда пользователь выберет «Пользовательская».

Создание таблицы нот

Функция createNoteTable() строит массив noteFreq для хранения массива объектов, представляющих каждую октаву. Каждая октава, в свою очередь, имеет одно именованное свойство для каждой ноты в этой октаве; имя свойства — имя ноты (например, «C#» для обозначения до-диез), а значение — частота этой ноты в Герцах.

function createNoteTable() {
  const noteFreq = [];
  for (let i=0; i< 9; i++) {
    noteFreq[i] = [];
  }

  noteFreq[0]["A"] = 27.500000000000000;
  noteFreq[0]["A#"] = 29.135235094880619;
  noteFreq[0]["B"] = 30.867706328507756;

  noteFreq[1]["C"] = 32.703195662574829;
  noteFreq[1]["C#"] = 34.647828872109012;
  noteFreq[1]["D"] = 36.708095989675945;
  noteFreq[1]["D#"] = 38.890872965260113;
  noteFreq[1]["E"] = 41.203444614108741;
  noteFreq[1]["F"] = 43.653528929125485;
  noteFreq[1]["F#"] = 46.249302838954299;
  noteFreq[1]["G"] = 48.999429497718661;
  noteFreq[1]["G#"] = 51.913087197493142;
  noteFreq[1]["A"] = 55.000000000000000;
  noteFreq[1]["A#"] = 58.270470189761239;
  noteFreq[1]["B"] = 61.735412657015513;
  // …

Несколько октав опущены для краткости.

  noteFreq[7]["C"] = 2093.004522404789077;
  noteFreq[7]["C#"] = 2217.461047814976769;
  noteFreq[7]["D"] = 2349.318143339260482;
  noteFreq[7]["D#"] = 2489.015869776647285;
  noteFreq[7]["E"] = 2637.020455302959437;
  noteFreq[7]["F"] = 2793.825851464031075;
  noteFreq[7]["F#"] = 2959.955381693075191;
  noteFreq[7]["G"] = 3135.963487853994352;
  noteFreq[7]["G#"] = 3322.437580639561108;
  noteFreq[7]["A"] = 3520.000000000000000;
  noteFreq[7]["A#"] = 3729.310092144719331;
  noteFreq[7]["B"] = 3951.066410048992894;

  noteFreq[8]["C"] = 4186.009044809578154;
  return noteFreq;
}

Результат — массив noteFreq, с объектом для каждой октавы. Каждый объект октавы имеет именованные свойства, где имя свойства — имя ноты (например, «C#» для обозначения до-диез), а значение свойства — частота ноты в Герцах. Частично полученный объект выглядит так:

Октава Ноты
0 "A" ⇒ 27.5 "A#" ⇒ 29.14 "B" ⇒ 30.87
1 "C" ⇒ 32.70 "C#" ⇒ 34.65 "D" ⇒ 36.71 "D#" ⇒ 38.89 "E" ⇒ 41.20 "F" ⇒ 43.65 "F#" ⇒ 46.25 "G" ⇒ 49 "G#" ⇒ 51.9 "A" ⇒ 55 "A#" ⇒ 58.27 "B" ⇒ 61.74
2 . . .

С этой таблицей мы можем легко найти частоту для данной ноты в определённой октаве. Если нам нужна частота ноты G# в октаве 1, мы используем noteFreq[1]["G#"] и получаем значение 51,9 в результате.

Примечание: Значения в приведённой выше таблице округляются до двух десятичных знаков.

Создание клавиатуры

Функция setup() отвечает за построение клавиатуры и подготовку приложения к воспроизведению музыки.

function setup() {
  noteFreq = createNoteTable();

  volumeControl.addEventListener("change", changeVolume, false);

  mainGainNode = audioContext.createGain();
  mainGainNode.connect(audioContext.destination);
  mainGainNode.gain.value = volumeControl.value;

  // Create the keys; skip any that are sharp or flat; for
  // our purposes we don't need them. Each octave is inserted
  // into a <div> of class "octave".

  noteFreq.forEach((keys, idx) => {
    const keyList = Object.entries(keys);
    const octaveElem = document.createElement("div");
    octaveElem.className = "octave";

    keyList.forEach((key) => {
      if (key[0].length === 1) {
        octaveElem.appendChild(createKey(key[0], idx, key[1]));
      }
    });

    keyboard.appendChild(octaveElem);
  });

  document
    .querySelector("div[data-note='B'][data-octave='5']")
    .scrollIntoView(false);

  sineTerms = new Float32Array([0, 0, 1, 0, 1]);
  cosineTerms = new Float32Array(sineTerms.length);
  customWaveform = audioContext.createPeriodicWave(cosineTerms, sineTerms);

  for (let i = 0; i < 9; i++) {
    oscList[i] = {};
  }
}

setup();
  1. Таблица, сопоставляющая имена нот и октавы с их частотами, создаётся путём вызова createNoteTable().
  2. Обработчик событий создаётся (вызывая нашего старого друга addEventListener()) для обработки событий change основного элемента управления громкостью. Это обновит громкость основного узла усиления до нового значения управления.
  3. Далее мы перебираем каждую октаву в таблице частот нот. Для каждой октавы мы используем Object.entries() для получения списка нот в этой октаве.
  4. Создаём элемент <div> для размещения нот этой октавы (чтобы между октавами было немного пространства), и устанавливаем его имя класса на «octave».
  5. Для каждой клавиши в октаве мы проверяем, содержит ли имя ноты более одного символа. Мы их пропускаем, потому что в этом примере мы опускаем диезы. Если имя ноты содержит только один символ, то мы вызываем createKey(), задавая строку ноты, октаву и частоту. Возвращаемый элемент добавляется к элементу октавы, созданному на шаге 4.
  6. Когда все элементы октав будут построены, они добавляются к клавиатуре.
  7. После построения клавиатуры мы прокручиваем ноту «B» в октаве 5 в область видимости; это обеспечит отображение среднего до вместе с окружающими клавишами.
  8. Затем создаётся новая пользовательская форма волны с помощью BaseAudioContext.createPeriodicWave(). Эта форма волны будет использоваться всякий раз, когда пользователь выберет «Пользовательская» в элементе управления выбором формы волны.
  9. Наконец, список генераторов колебаний инициализируется, чтобы он был готов к приёму информации, идентифицирующей, какие генераторы колебаний связаны с какими клавишами.

Создание клавиши

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

function createKey(note, octave, freq) {
  const keyElement = document.createElement("div");
  const labelElement = document.createElement("div");

  keyElement.className = "key";
  keyElement.dataset["octave"] = octave;
  keyElement.dataset["note"] = note;
  keyElement.dataset["frequency"] = freq;
  labelElement.appendChild(document.createTextNode(note));
  labelElement.appendChild(document.createElement("sub")).textContent = octave;
  keyElement.appendChild(labelElement);

  keyElement.addEventListener("mousedown", notePressed, false);
  keyElement.addEventListener("mouseup", noteReleased, false);
  keyElement.addEventListener("mouseover", notePressed, false);
  keyElement.addEventListener("mouseleave", noteReleased, false);

  return keyElement;
}

После создания элементов, которые будут представлять клавишу и её метку, мы настраиваем элемент клавиши, установив его класс на «key» (что определяет его внешний вид). Затем мы добавляем атрибуты data-*, которые содержат октаву клавиши (атрибут data-octave), строку, представляющую воспроизводимую ноту (атрибут data-note), и частоту (атрибут data-frequency). Это позволит нам легко получить эту информацию при необходимости при обработке событий.

Воспроизведение музыки

Проигрывание тона

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

function playTone(freq) {
  const osc = audioContext.createOscillator();
  osc.connect(mainGainNode);

  const type = wavePicker.options[wavePicker.selectedIndex].value;

  if (type === "custom") {
    osc.setPeriodicWave(customWaveform);
  } else {
    osc.type = type;
  }

  osc.frequency.value = freq;
  osc.start();

  return osc;
}

playTone() начинается с создания нового OscillatorNode, вызвав метод BaseAudioContext.createOscillator(). Затем мы подключаем его к основному узлу усиления, вызвав метод connect() нового осциллятора; это указывает осциллятору, куда отправлять свой выход. Таким образом, изменение усиления основного узла усиления повлияет на громкость всех генерируемых тонов.

Затем мы получаем тип используемой формы волны, проверив значение контрола выбора формы волны на панели настроек. Если пользователь выбрал "custom", мы вызываем OscillatorNode.setPeriodicWave(), чтобы настроить осциллятор на использование нашей пользовательской формы волны. Это автоматически устанавливает тип осциллятора type на custom. Если в волновом выборе выбран другой тип формы волны, мы устанавливаем тип осциллятора на значение выбора; это значение будет одним из sine, square, triangle, и sawtooth.

Частота осциллятора устанавливается на значение, указанное в параметре freq, путем установки значения объекта AudioParam OscillatorNode.frequency. Затем, наконец, осциллятор запускается, чтобы начать воспроизводить звук, вызвав унаследованный метод AudioScheduledSourceNode.start() осциллятора.

Проигрывание ноты

При возникновении события mousedown или mouseover на клавише мы хотим начать воспроизведение соответствующей ноты. Функция notePressed() используется в качестве обработчика событий для этих событий.

function notePressed(event) {
  if (event.buttons & 1) {
    const dataset = event.target.dataset;

    if (!dataset["pressed"] && dataset["octave"]) {
      const octave = Number(dataset["octave"]);
      oscList[octave][dataset["note"]] = playTone(dataset["frequency"]);
      dataset["pressed"] = "yes";
    }
  }
}

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

Если кнопка мыши нажата, мы получаем атрибут dataset нажатой клавиши; это упрощает доступ к пользовательским атрибутам данных элемента. Мы ищем атрибут data-pressed; если его нет (что указывает на то, что нота еще не воспроизводится), мы вызываем playTone(), чтобы начать воспроизведение ноты, передав значение атрибута элемента data-frequency. Возвращаемый осциллятор хранится в oscList для дальнейшего использования, а data-pressed устанавливается на yes для обозначения того, что нота воспроизводится, чтобы мы не запускали ее снова при следующем вызове.

Остановка тона

Функция noteReleased() — это обработчик событий, вызываемый, когда пользователь отпускает кнопку мыши или перемещает курсор мыши вне текущей воспроизводимой клавиши.

function noteReleased(event) {
  const dataset = event.target.dataset;

  if (dataset && dataset["pressed"]) {
    const octave = Number(dataset["octave"]);

    if (oscList[octave] && oscList[octave][dataset["note"]]) {
      oscList[octave][dataset["note"]].stop();
      delete oscList[octave][dataset["note"]];
      delete dataset["pressed"];
    }
  }
}

noteReleased() использует пользовательские атрибуты data-octave и data-note для поиска осциллятора клавиши, затем вызывает унаследованный метод stop() осциллятора, чтобы остановить воспроизведение ноты. Наконец, запись oscList для ноты очищается, и атрибут data-pressed удаляется из элемента клавиши (как определено в event.target), чтобы указать, что нота в данный момент не воспроизводится.

Изменение основного уровня громкости

Ползунок громкости на панели настроек предоставляет интерфейс для изменения значения усиления основного узла усиления, тем самым изменяя громкость всех воспроизводимых нот. Метод changeVolume() — это обработчик события change на ползунке.

function changeVolume(event) {
  mainGainNode.gain.value = volumeControl.value;
}

Это устанавливает значение AudioParam основного узла усиления на новое значение ползунка.

Поддержка клавиатуры

Код ниже добавляет обработчики событий keydown и keyup для обработки ввода с клавиатуры. Обработчик событий keydown вызывает notePressed() для начала воспроизведения ноты, соответствующей нажатой клавише, а обработчик событий keyup вызывает noteReleased() для остановки воспроизведения ноты, соответствующей отпущенной клавише.

const synthKeys = document.querySelectorAll(".key");
const keyCodes = [
  "Space",
  "ShiftLeft", "KeyZ", "KeyX", "KeyC", "KeyV", "KeyB", "KeyN", "KeyM", "Comma", "Period", "Slash", "ShiftRight",
  "KeyA", "KeyS", "KeyD", "KeyF", "KeyG", "KeyH", "KeyJ", "KeyK", "KeyL", "Semicolon", "Quote", "Enter",
  "Tab", "KeyQ", "KeyW", "KeyE", "KeyR", "KeyT", "KeyY", "KeyU", "KeyI", "KeyO", "KeyP", "BracketLeft", "BracketRight",
  "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "Digit0", "Minus", "Equal", "Backspace",
  "Escape",
];
function keyNote(event) {
  const elKey = synthKeys[keyCodes.indexOf(event.code)];
  if (elKey) {
    if (event.type === "keydown") {
      elKey.tabIndex = -1;
      elKey.focus();
      elKey.classList.add("active");
      notePressed({ buttons: 1, target: elKey });
    } else {
      elKey.classList.remove("active");
      noteReleased({ buttons: 1, target: elKey });
    }
    event.preventDefault();
  }
}
addEventListener("keydown", keyNote);
addEventListener("keyup", keyNote);

Результат

В совокупности результат — это простая, но рабочая, клавиатура для нажатия:

См. также

  • API Веб-аудио
  • OscillatorNode
  • GainNode
  • AudioContext

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Simple_synth

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API