Obs.: Eu sei que eu prometi, mas não vai ser hoje que vai ter um texto não técnico sobre o tananã. Desculpa aí, fica pra próxima. Hoje vou falar de código e também um pouco de análise de requisitos. Apesar disso, manterei o esforço de ser o mais didático possível para aqueles que não entendem código.
Faz alguns meses que escrevi do que era o tananã e sobre a escolha do electron. O projeto ficou parado por boa parte desse tempo, mas no último mês, houve progresso.
# O estado do app até então
Apesar de o design estar sofrível (novidades em breve), o app está mais ou menos usável. Você pega um arquivo music xml e ele tem lá o “player” que te diz em tempo real (de acordo com o bpm do arquivo) que notas você deveria estar tocando (ou mesmo cantando). Com botão de play/pause e um de stop, o app funcionava com um mínimo de usabilidade.
Porém não havia um autoscroll ainda, ou seja, você precisaria rolar para baixo enquanto treina sua leitura de partitura, o que não é lá um upgrade em relação a uma partirua num papel em que se precisa trocar de página, não é mesmo?
# Um bug que me assombrou por 4 meses
Quando você está assistindo um vídeo, normalmente a barra de espaço serve como uma pausa. Porém, agora no seu browser, experimente apertar a barra de espaço. O comportamento padrão é rolar para baixo. No caso do tananã esse comportamento precisa ser eliminado, porém, ao colocar a função play/pause na barra de espaço, eu não conseguia resolver isso. Até eu me ligar que eu estava usando o evento keyup
e o evento padrão de scroll é o keydown
. Ou seja, eu estava chamando o famoso preventDefault()
no vazio. Aqui está o commit do fix. Dorme com esse barulho aí, essa é a realidade da profissão: 11 anos programando não te tornam imune a erros bestas.
# Resolvendo o autoscroll
Falando em scroll, como eu comentei antes, seria legal que o rolamento da tela fosse automático, acompanhando a execução da partitura. A API que estou usando como dependência é a Open Sheet Music Display (OSMD, para simplificar). Ela fornece o cursor, que eu vou avançando no tempo correto com o player. A cada atualização do cursor há uma chance de ele ter descido para a linha de baixo e eventualmente sumir da tela. Para isso, eu precisava customizar a velocidade do scrollTo
nativo do javascript, o que não é possível. Por causa disso, foi necessário usar o requestAnimationFrame
e implementar algo com comportamento semelhante, já que o scroll precisa ser leve e visível para o usuário. Eu usei algumas referências que achei por aí (principalmente esse último link) e não está 100%, mas acho que já está bom o suficiente para testes em beta. Caso você puder me ajudar, por favor, vai lá no gitlab 😉
# A feature mais legal de implementar até agora
Outra coisa importante é a pessoa clicar numa nota para começar a praticar a partir da mesma. Infelizmente a OSMD não possui função de clique no cursor exposta. Até discutiram o desenvolvimento disso um ano atrás, mas não dá pra eu ficar esperando os gringos resolverem meu problema pra mim né? Então eu resolvi tentar encontrar alguma solução que pudesse ser uma constribuição minha ao software deles. Até eu me ligar que eu poderia fazer uma gambiarra muito divertida. Viva🎉 as🎉 gambiarras🎉!
O código em que eu resolvo a questão tem apenas 41 linhas. Vou explicar ele mais ou menos aqui. Primeiro, eu precisava, dado o evento de clique, “passear” o cursor desde o começo da partitura até encontrar a nota que estivesse “perto o suficiente” do evento de clique. Então comecei logo com o código abaixo:
// a primeira função recebe o player, que é um objeto meu
// tá lá no código, na classe Player
const moveCursor = player => (ev) => {
const { cursor } = player.osmd
// se o player estiver tocando, não fazer nada
if (player.playing) {
return
}
// reiniciar o player desde o começo, e então pausá-lo
restartAndPause(player)
// um loop infinito 😆
while (true) {
// anda com o cursor para a próxima posição e pega sua posição
const bounding = cursorNextPosition(cursor)
// quebra o loop infinito quando essa posição esitver perto da posição do evento
// ou seja, a variável ev
if (cursorIsCloseEnough(ev, bounding)) break
}
// a função de restartAndPause precisaria esconder o cursor,
// já que o usuário não iria querer ficar vendo um cursor navegando
// mega rápido pela partitura até chegar onde ele clicou né kkkk
// por isso, ao final do processo, é para mostrar o cursor
cursor.show()
}
const moveCursorOnClick = (player) => player.osmd.container
.addEventListener('click', moveCursor(player))
A partir daí, implementar as funções que eu previ antes for até fácil:
const restartAndPause = (player) => {
player.end()
player.play()
player.pause()
player.osmd.cursor.hide()
}
// as constantes foram determinadas pelo tamanho do próprio cursor
const cursorIsCloseEnough = (evPos, cursorPos) =>
(Math.abs(evPos.x - cursorPos.x) <= DELTA_X) &&
(Math.abs(evPos.y - cursorPos.y) <= DELTA_Y)
const cursorNextPosition = (cursor) => {
// para pegar o bounding, o cursor precisa estar "aparecendo",
// mesmo que seja rápido demais
cursor.show()
// essa função next() é da própria API do Open Sheet Music Display
cursor.next()
// função nativa do javascript que pega a posição x/y de um elemento HTML
const bounding = cursor.cursorElement.getBoundingClientRect()
// esconder o elemento e retornar a posição
cursor.hide()
return bounding
}
Vocês podem conferir o resultado no GIF que eu tuitei outro dia, todo empolgado:
Domingo produtivo 🥳. Agora quando você clica numa nota, o cursor move pra ela.#tananã #devlog pic.twitter.com/sMc1soQT5Y
— graciano codes (@wtfgraciano) August 18, 2019
Quem quiser ajudar (por enquanto programadores, depois vou falar com os músicos 😉) dá uma olhada no que tem pra fazer para a versão beta aqui e se quiser pode ir no nosso servidor do discord. Forte abraço!