Compose + Wear OS: Afinador Cromático

Exemplo prático do poder do Jetpack Compose

Arildo Borges Jr
CodeInLab
5 min readDec 1, 2021

--

imagem criada por freepik — br.freepik.com

Nesse artigo vou abordar o desenvolvimento de um afinador cromático para Wear OS, versão do android projetada para dispositivos wearables — como smartwatches, usando Jetpack Compose 😎.

Antes de mais nada, você sabe o que é um afinador cromático? Não? Vamos lá:

Afinador cromático serve para detectar a altura das 12 notas musicais, permitindo que instrumentos sejam afinados corretamente.

Todas as notas musicais possuem sua frequência de vibração específica. A nota A4 (lá), por exemplo, oscila a 440Hz. Tendo isso como base, nosso objetivo é desenvolver um aplicativo que detecte a frequência que está sendo tocada e o quão acima ou abaixo ela está da frequência específica de cada uma das 12 notas, para que o músico consiga afinar seu instrumento corretamente. Bora?

Interface

A interface do nosso app será bastante simples:

imagem da UI, demonstrando minhas fortes skills de design… 😅

O primeiro estado, com fundo verde, indica que a nota tocada tem a mesma frequência da nota A# (lá sustenido), ou seja, a nota está afinada.

Já o segundo, além da cor vermelha para indicar que a frequência não está correta, também exibe setas laterais para indicar se o músico deve aumentar ou diminuir a altura da nota, a fim de chegar à frequência correta.

Configurando Projeto

Não tem muito mistério para criar um projeto com suporte ao Wear OS e Compose. Você pode criar um novo projeto, da maneira usual, e adicionar essas três tags ao AndroidManifest.xml:

Wear Compose

Além dos pacotes convencionais do Compose, também existem pacotes específicos para dispositivos wearables:

https://developer.android.com/jetpack/androidx/releases/wear-compose

Vamos usar dois desses pacotes e, aproveitando que estamos configurando as dependências do projeto, adicionaremos uma biblioteca de DSP chamada TarsosDSP que, entre outros recursos, possui reconhecimento de frequências.

  • Você pode baixá-la aqui

O bloco dependencies do nosso build.gradle ficará o seguinte:

Pronto, dependências adicionadas, agora o projeto está pronto para começarmos o desenvolvimento. Essa é a nossa MainActivity:

Note que estendemos de ComponentActivity() e não mais de AppCompatActivity()

Uma das coisas mais legais do Compose para Wear OS é que ele nos disponibiliza um Scaffold próprio e algumas views úteis, como a TimeText():

imagem com preview do nosso hello world no emulador

Com apenas 13 linhas, nosso app já ficou assim e simmm, minhas amigas, o TimeText() é um relógio prontinho 😎.

Agora vamos implementar a UI e o @Preview para cada um dos estados da tela:

Feito isso, teremos os seguintes previews:

imagem com preview dos três estados de tela

Note que no código acima a função TunerScreen() recebe uma TunerState(), que é uma sealed class que criei para facilitar o gerenciamento dos estados:

Dessa forma, através de apenas uma classe, temos todas as informações necessárias para propagarmos as mudanças e atualizarmos nossa view 😎.

Detecção de frequências

A biblioteca TarsosDSP possui a interface PitchDetectionHandler que, entre diversos recursos, nos disponibiliza a frequência e o volume do som que está sendo captado. Com isso, basta usar esses valores obtidos e implementar a lógica de comparação com os valores de referência que temos para cada uma das 12 notas e suas oitavas:

Fonte: https://www.treinaweb.com.br/blog/gerando-sons-com-a-web-audio-api-do-javascript

Dentro da PitchDetectionHandler, recebemos a frequência (em Hz) e verificamos se devemos atualizar o estado da tela. A função shouldUpdateTunerState() é responsável por essa parte, validar se o áudio captado possui um volume mínimo, para evitar o processamento de ruídos e se a frequência captada é valida.

Caso passe nessa validação, chamamos a função getCurrentPitchState(), que é responsável por nos devolver uma instância de TunerState() para, em seguida, propagamos o resultado para o LiveData _tunerState:

A lógica da função getCurrentPitchState() é bem simples:

  • Verificar qual das frequências, entre todas as notas, é mais próxima da frequência capturada;
  • Pegar a nota correspondente à frequência mais próxima;
  • Calcular a diferença entre a frequência de entrada e a frequência de referência;
  • Retornar uma instância de TunerState(), levando em consideração essa diferença, se está dentro do aceitável, abaixo ou acima.

Como instrumentos de cordas sofrem variações de frequência por causa da amplitude da vibração da corda, logo temos que adotar uma faixa aceitável para um som ser considerado afinado. Para isso, vamos usar a norma ISO16, que prevê uma tolerância de ±0.5Hz.

Essa lógica está implementada na linha 12 do código acima, na extension isInPermittedTolerance(), que nada mais é que a seguinte linha:

LiveData.observeAsState()

Outro ponto que vale a pena a ser comentado é sobre como nossa ui observa as informações propagadas ao LiveData. O Compose possui a extension LiveData.observeAsState(), que tem seu funcionamento bem parecido com a forma tradicional de se observar mudanças no LiveData, com a interface Observer. A diferença é que usando esse approach, a tela é recomposta automaticamente e o código fica bem mais limpo:

E agora? Bom, agora podemos pular para a próxima etapa, que é justamente ver o app funcionando (ou não 😅).

Funcionamento

Para validar o funcionamento do nosso afinador, vamos usar um app gerador de tom reproduzindo frequências específicas, a fim de verificar se o afinador está se comportando conforme o esperado. Como sabemos que a nota A4 tem frequência de 440Hz, vamos usar ela em nossos casos de teste:

  • Frequência 439.5Hz deve exibir Nota A afinada
  • Frequência 440.5Hz deve exibir Nota A afinada
  • Frequência 439.4Hz deve exibir Nota A desafinada para baixo
  • Frequência 440.6Hz deve exibir Nota A desafinada para cima
Screenshots tiradas do meu Galaxy Watch4 + celular

Ok, está funcionando 😎

Você pode conferir o código completo em:

Qualquer sugestão ou dúvida, fique a vontade para deixar seu comentário.
Obrigado por ler e até a próxima!

--

--