Tutorial e exemplo: Teclado de Sintetizador Simples
Este artigo apresenta o código e uma demonstração funcional de um teclado que você pode tocar usando seu mouse. O teclado lhe permite alternar entre formas de onda padrões e customizadas. Esse exemplo utiliza das seguintes interfaces de Web API: AudioContext
, OscillatorNode
, PeriodicWave
, e GainNode
.
Já que OscillatorNode
é baseado no AudioScheduledSourceNode
, isso até certo ponto também é um exemplo pra isto.
O Teclado Visual
HTML
Existem três componentes primários para o display do nosso teclado virtual. O primeito do qual é o teclado musical em si. Nós extraimos em um par de elementos <div>
aninhados para permitir a rolagem horizontal caso as teclas não encaixem na tela.
O Teclado
Primeiro, criamos o espaço no qual construiremos o teclado. Estaremos construindo o teclado programaticamente, considerando que ao fazer desse jeito teremos a flexibilidade de configurar cada tecla conforme determinamos as informações apropriadas para tecla correspondente. No nosso caso, pegamos a frequência de cada tecla através de uma tabela, mas poderia ser calculado de forma algoritmica também.
<div class="container">
<div class="keyboard"></div>
</div>
O <div>
nomeado de "container"
é a barra de rolagem que permite o teclado ser rolado horizontalmente se for largo demais para o espaço disponivel. As teclas em si serão inseridas no bloco de classe "keyboard"
.
A barra de opções
Abaixo do teclado, colocaremos alguns controles para configurar o camada. Por enquanto, teremos dois controles: Um para controlar o volume e outro para selecionar a forma de onda periodica usada ao gerar as notas.
O controle de volume
Primeiro criamos o <div>
para conter a barra de opções, para ser personalizado conforme preciso. Então estabelecemos uma caixa que será apresentada no lado esquerdo da barra e colocar um rotulo e um elemento <input>
do tipo "range"
. O elemento range será tipicamente apresentado como o controle da barra de rolagem ; configuramos ele para permitir qualquer valor entre 0.0 e 1.0 em cada posição.
<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>
Especificamos um valor padrão de 0.5, e provemos um elemento <datalist>
no qual é conectado ao range usando o atributo name
para achar uma lista de opções cujo ID encaixa; nesse caso, o conjunto de informações é nomeado de "volume"
. isso nos permite prover um conjunto de valores comuns e strings especiais que o browser pode de forma opcional escolher mostrar de alguma maneira; e então atribuimos nomes aos valores 0.0 ("Mute") e 1.0 ("100%").
A seleção de forma de onda
E no lado da barra de configurações, colocamos um rótulo e um elemento <select>
nomeado de "waveform"
cujas opções correspondem as formas de onda disponiveis.
<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>
JavaScript
O código em JavaScript começa inicializando algumas váriaveis.
let audioContext = new (window.AudioContext || window.webkitAudioContext)();
let oscList = [];
let masterGainNode = null;
audioContext
é colocado para referenciar o objeto globalAudioContext
(ouwebkitAudioContext
se necessário).oscillators
está colocado para conter uma lista de todos os osciladores atualmente tocando. Ele começa nulo, afinal não há nenhum oscilador tocando ainda.masterGainNode
é colocado como nulo; durante o processo de setup, ele será configurado para contar umGainNode
no quall todos os osciladores irão se conectar para permitir o volume geral a ser controlado por apenas uma barra de rolagem.
let keyboard = document.querySelector(".keyboard");
let wavePicker = document.querySelector("select[name='waveform']");
let volumeControl = document.querySelector("input[name='volume']");
Referencias aos elementos que precisaremos acessar são obtidas através dp:
keyboard
que é o elemento que irá alojar as teclas.wavePicker
é o elemento<select>
usado para seleção da forma de onda das notas.volumeControl
É o elemento<input>
(do tipo"range"
) usado para controlar o volume geral.
let noteFreq = null;
let customWaveform = null;
let sineTerms = null;
let cosineTerms = null;
Enfim, variaveis globais que serão usadas quando as formas de onda são criadas:
noteFreq
será uma matriz de matrizes; cada matriz representa uma oitava, cada uma possuindo um valor nota daquela oitava. O valor de cada é a frequência, em Hertz, do tom da nota.customWaveform
será arrumado como umPeriodicWave
descrevendo a forma de onda quando o usuário selecionar "Custom" na seleção de forma de onda.sineTerms
ecosineTerms
será utilizado para guardar a informação para gerar a forma de onda; cada um irá conter uma matriz que será gerada caso o usuário escolha "Custom".
Criando a tabela de notas
A função createNoteTable()
constrói a matriz noteFreq
para conter uma matriz de objetos representando cada oitava. Cada oitava, possui uma propriedade para cada nota nessa oitava; O nome dessa propriedade é o nome da nota (utilizando da notação em inglês, como "C" para representar "dó"), e o valor é a frequência, em Hertz, daquela nota.
function createNoteTable() {
let 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;
... várias oitavas não mostradas para manter breve ...
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;
}
O resultado é uma matriz, noteFreq
, com um objeto para cada oitava. Cada objeto de oitava tem propriedades nomeadas nela onde a propriedade é o nome da nota com a notação em inglês (Como "C" para representar "dó") e o valor da propriedade é a frequência da nota em Hertz.. o objeto resultando se parece com isso:
Octave | Notes | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
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 | . . . |
Com esta tabela no lugar, podemos descobrir a frequência para uma dada nota em uma oitava particular relativamente fácil. Se queremos a frequência pra nota G# na primeira oitava, nós simplesmente usamos noteFreq[1]["G#"]
e conseguimos o valor 51.9 como resultado.
Nota: Os valores na tabela de exemplo acima foram arredondados para duas casas decimais.
Construindo o teclado
A função setup()
é responsavel por construir o teclado e preparar a aplicação para tocar a música.
function setup() {
noteFreq = createNoteTable();
volumeControl.addEventListener("change", changeVolume, false);
masterGainNode = audioContext.createGain();
masterGainNode.connect(audioContext.destination);
masterGainNode.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(function (keys, idx) {
let keyList = Object.entries(keys);
let octaveElem = document.createElement("div");
octaveElem.className = "octave";
keyList.forEach(function (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 (i = 0; i < 9; i++) {
oscList[i] = {};
}
}
setup();
- A tabela que mapeia o nome e oitavas das notas para suas respectivas frequências é criado ao chamar
createNoteTable()
. - Um manipulador de eventos é estabelecido ao chamar nosso velho amigo
addEventListener()
para cuidar dos eventos dounsupported templ: eventno controle de ganho geral. Isso vai simplesmente atualizar o módulo de ganho de volume para o novo valor. - Em seguida, nós replicamos cada oitava na tabela de frequências das notas. Para cada oitava, usamos
Object.entries()
para conseguir uma lista de notas daquela oitava. - Criar um
<div>
para contar as notas daquela oitava (para ter um pouco de espaço entre as oitavas), e mudar o nome de classe para "octave". - Para cada tecla na oitava, checamos para ver se o nome daquela nota há mais de um caractere. Nós pulamos essas, pois estamos deixando notas sustenidas de fora deste exemplo. Do contrário, chamamos
createKey()
, especificando uma string, oitava, e frequência. O elemento retornado é anexado na elemento da oitava criada no passo 4. - Quando o elemento da oitava é construido, é então anexada ao teclado.
- Uma vez que o teclado foi construido, nós rolamos para nota "B" na quinta oitava; isso tem o efeito de garantir que o C médio é visivel junto das notas ao redor.
- Então uma forma de onda customizada é construida usando
AudioContext.createPeriodicWave()
. Essa forma de onda será usada toda vez que o usuário selecionar "Custom" da seleção de formas de onda. - Enfim, a lista de osciladores é iniciada para garantir que está pronta para receber informação identificando quais osciladores estão associados com que teclas.
Criando uma tecla
A função createKey()
é chamada toda vez que queremos que uma tecla seja apresentada no nosso teclado virtual. Ela cria elementos da tecla e seu rótulo, adiciona informação dos atributos ao elemento para uso posterior, e coloca modificadores de eventos para os eventos que nos importam.
function createKey(note, octave, freq) {
let keyElement = document.createElement("div");
let labelElement = document.createElement("div");
keyElement.className = "key";
keyElement.dataset["octave"] = octave;
keyElement.dataset["note"] = note;
keyElement.dataset["frequency"] = freq;
labelElement.innerHTML = note + "<sub>" + octave + "</sub>";
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;
}
Após criar os elementos representando as teclas e seus rótulos, nós configuramos o elemento das teclas ao configurar sua classe para "key" (Que estabelece a aparência). Então adicionamos atributos data-*
que contém a string da oitava da nota (attribute data-octave
), representando a nota a ser tocada (attribute data-note
), e frequência (attribute data-frequency
) em Hertz. Isso irá nos permitir facilmente pegar informação conforme necessário ao cuidar de eventos.
Fazendo música
Tocando um tom
O trabalho da função playTone()
é tocar um tom em uma dada frequência. Isso será usado pelo modificador para eventos acionados nas teclas do teclado, para que toquem as notas apropriadas.
function playTone(freq) {
let osc = audioContext.createOscillator();
osc.connect(masterGainNode);
let type = wavePicker.options[wavePicker.selectedIndex].value;
if (type == "custom") {
osc.setPeriodicWave(customWaveform);
} else {
osc.type = type;
}
osc.frequency.value = freq;
osc.start();
return osc;
}
O playTone()
começa criando um novo OscillatorNode
ao chamar o método AudioContext.createOscillator()
. Então conectamos ele para o módulo de ganha geral ao chamar o novo método de osciladores OscillatorNode.connect()
method;, Que determina ao oscilador onde ele irá mandar seu output. Ao fazer isso, mudar o valor do ganho do módulo de ganho geral irá mudar o volume de todos os toms gerados.
Então conseguimos o tipo de forma de onda para usar ao checar o valor do controle de seleção de formas de onda na barra de opções. Se o usuário estiver colocado como "custom"
, chamamos OscillatorNode.setPeriodicWave()
para configurar os osciladores para usar nossa forma de onda customizada. Fazer isso automáticamente coloca o type
do oscilador como custom
. Se qualquer outro tipo de forma de onda é selecionado na seleção de formas de ondas, nós simplesmente colocamos os tipos de osciladores no valor da seleção, esse valor será um entre sine
, square
, triangle
, e sawtooth
.
A frequência do oscilador é colocada no valor especificado no paramêtro freq
ao colocar o valor dos objetos Oscillator.frequency
AudioParam
. Então, enfim, o oscilador é iniciado e começa a produzir sons ao chamar o método AudioScheduledSourceNode.start()
.
Tocando uma nota
Quando o evento unsupported templ: event ou mouseover
ocorre em uma tecla, queremos que toque a nota correspondente. A função notePressed()
é usada como o modificador de eventos para esses eventos.
function notePressed(event) {
if (event.buttons & 1) {
let dataset = event.target.dataset;
if (!dataset["pressed"]) {
let octave = +dataset["octave"];
oscList[octave][dataset["note"]] = playTone(dataset["frequency"]);
dataset["pressed"] = "yes";
}
}
}
Começamos checando se o botão esquerdo do mouse é pressionado, por dois motivos. Primeiro, queremos que apenas o botão esquerdo acione as notas. Segundo, e mais importante, estamos usando isso para cuidar do unsupported templ: event para casos onde o usuário arrasta de tecla a tecla, e só queremos tocar uma nota se o mouse estiver pressionado quando entrar no elemento.
Se o botão do mouse estiver de fato sendo pressionado, recebemos o atributo de tecla pressionada dataset
; isso torna fácil o acesso das informações de atributo customizadas no elemento. Procuramos por um atributo data-pressed
; caso não haja um(o que indica que a nota não está tocando ainda), chamamos playTone()
para começar a tocar a nota, passando no valor dos elementos do atributo data-frequency
. O valor retornado do oscilador é guardado no oscList
para refêrencia futura, e data-pressed
é colocado como yes
para indicar que a nota está tocando para que não iniciemos novamente na próxima vez que isso for chamado.
Parando um tom
A função noteReleased()
é o modificador de eventos chamado quando o usuário solta o botão do mouse ou move o mouse para fora da tecla que ele está tocando.
function noteReleased(event) {
let dataset = event.target.dataset;
if (dataset && dataset["pressed"]) {
let octave = +dataset["octave"];
oscList[octave][dataset["note"]].stop();
delete oscList[octave][dataset["note"]];
delete dataset["pressed"];
}
}
noteReleased()
usa os atributos customizados data-octave
and data-note
para procurar os osciladores das teclas, e então chama o método de oscilador stop()
para parar de tocar a nota. Finalmente, a entrada oscList
para nota é limpa e o atributo data-pressed
é removido do elemento da tecla (como identificado pelo event.target
), para indicar que a nota não está tocando no momento.
Mudando o volume geral
A barra de rolagem do volume na barra de opções dá uma simples interface para mudar o valor do ganho no módulo de ganho geral, então mudando o volume de todas as notas sendo tocadas. O metódo changeVolume()
é o modificador do evento unsupported templ: event na barra de rolagem.
function changeVolume(event) {
masterGainNode.gain.value = volumeControl.value;
}
Isso simplesmente coloca o valor do módulo de ganho geral gain
AudioParam
para o novo valor na barra de rolagem.
Resultado
Coloque tudo junto, o resultado é um simples e funcional teclado virtual que funciona com o clique: