scieee AI-readable full text Open interactive document viewer

Musikla: music and keyboard language

Silva, Pedro Miguel Oliveira da

Abstract

Nesta dissertação iremos estudar uma abordagem para a análise, criação e descrição de música através de uma Domain Specific Language (DSL). Trata-se de uma linguagem dinâmica, com todas as funcionalidades a que estamos habituados, tais como variáveis, funções, ciclos, condicionais. Para além disso, os dois fatores de diferenciação passam pela sintaxes especializadas para declaração de acom panhamentos musicais e de teclados virtuais. Esta linguagem deve depois poder ser avaliada e os seus resultados convertidos para diversos formatos, desde ficheiros de som, MIDI, ou reproduzir diretamente as notas para as colunas do computador. Com este intuito vamos analisar as linguagens já existentes neste espaço, bem como quais as funci onalidades que já implementam, e aquelas que consideramos estarem em falta. Como esses aspetos em mente, de seguida propomos uma linguagem que tente aproveitar as boas ideias daquilo que já existe, mais as nossas soluções para os novos desafios que encontramos. Introdu zimos também vários casos de estudo para demonstrarem as vantagens que acreditamos existirem na nossa abordagem. Finalmente descrevemos também o processo de desenvolvimento da linguagem, dividido em três fases principais: 1. O desenho da sintaxe, da sua gramática, e do parser. 2. A implementação do interpretador. 3. O desenvolvimento de uma biblioteca standard para ser incluída com a linguagem. A nível da sintaxe e da gramática, descrevemos sucintamente toda a linguagem. Damos particular atenção às expressões de declarações de acompanhamentos musicais e de teclados. Em termos grama ticais, são apenas expressões, ou seja, as suas sintaxes devem integrar-se homogeneamente no resto da linguagem. E como tal, podemos utilizá-las em qualquer sítio que onde podemos introduzir uma expressão, seja ela um número, uma string ou o que quer que for. Esta integração sem separação significa que todos os aspetos da linguagem têm de ser pensados de forma a coexistir sem problemas. Iremos por isso analisar quais os desafios encontrados pela introdução destas novas classes de expressões na gramática, e quais as soluções que foram tomadas para contornar essas situações. A nível do interpretador, discutimos várias das opções que poderiam ser escolhidas (interpretadores tree walk, máquinas bytecode, compilação JIT) bem como justificamos a nossa escolha de utilizar um interpretador tree-walk. A nível da biblioteca standard, descrevemos os vários formatos suportados, quer de input, quer de output, bem como os mecanismos providenciados para a utilização de teclados, como grelhas e buffers. No final, descrevemos como correr scripts escritos na nossa linguagem: através de uma aplicação de linha de comandos desenvolvida em Python, chamada musikla, publicada no Python Package Index (PyPI) 1, e cujo código é disponibilizado livremente no GitHub2.

Full text

Universidade do Minho Escola de Engenharia Pedro Miguel Oliveira da Silva Musikla Music and Keyboard Language Agosto, 2020 Universidade do Minho Escola de Engenharia Pedro Miguel Oliveira da Silva Musikla Music and Keyboard Language Dissertação de Mestrado Mestrado em Engenharia Informática Trabalho efetuado sob a orientação do(a) José João Dias de Almeida Agosto, 2020 DIREITOS DE AUTOR E CONDIÇÕES DE UTILIZAÇÃO DO TRABALHO POR TERCEIROS Este é um trabalho académico que pode ser utilizado por terceiros desde que respeitadas as regras e boas práticas internacionalmente aceites, no que concerne aos direitos de autor e direitos conexos. Assim, o presente trabalho pode ser utilizado nos termos previstos na licença abaixo indicada. Caso o utilizador necessite de permissão para poder fazer um uso do trabalho em condições não previstas no licenciamento indicado, deverá contactar o autor, através do RepositoriUM da Universidade do Minho. Licença concedida aos utilizadores deste trabalho Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en DECLARAÇÃO DE INTEGRIDADE Declaro ter atuado com integridade na elaboração do presente trabalho académico e confirmo que não recorri à prática de plágio nem a qualquer forma de utilização indevida ou falsificação de informações ou resultados em nenhuma das etapas conducente à sua elaboração. Mais declaro que conheço e que respeitei o Código de Conduta Ética da Universidade do Minho. Agradecimentos Quando estamos perante um momento importante da nossa vida, é fácil pensarmos nele como um momento circunspecto e isolado. Uma fase que acaba e outra que começa. Um antes e um depois. Mas a minha experiência até agora mostrou-me que na verdade, a minha vida tem sido mais um contínuo. E nesse contínuo estão um conjunto de pessoas sem o qual seria impossível pensar sonhar em fazer o que fiz. Por isso mesmo, em primeiro lugar, gostava de agradecer profundamente ao meu orientador de dissertação, o Professor José João Almeida, pela ajuda durante todo este processo. Sempre pronto a transmitir experiência e conhecimento, novas ideias, críticas construtivas e positivas, incentivando-me a agarrar todas as oportunidades, mas mais importante que tudo, encorajando-me sempre a seguir ao meu próprio ritmo e tomar o meu próprio caminho. Gostava também de agradecer aos meus colegas de curso, em especial à Sofia Silva, sempre pronta a ajudar para qualquer dúvida que eu tivesse e para me dar motivação para seguir em frente. Também não me posso esquecer de agradecer a todos os professores que tive, em particular aos professores Carlos Carvalho, Luís Cerejeira, Daniel Rego e Pedro Ribeiro, que me ensinaram, inspiraram, e se esforçaram para me providênciaram oportunidades e experiências que nunca irei esquecer, e que me abriram portas na vida que de outra forma me estariam fechadas. E mais importante que tudo, gostava de agradecer à minha família. À minha mãe, que sempre me apoiou durante todo o curso e fez o que fosse preciso para tornar a minha experiência o melhor possível. À minha tia, uma das pessoas mais trabalhadoras que conheço, que não pensa duas vezes antes de me ajudar em tudo o que eu precisar, e que está sempre presente para me apoiar. E finalmente ao meu irmão, a quem eu sempre quis seguir os passos, e que me fez querer começar a programar desde os doze anos de idade. Se não fosse por ele, não estaria a fazer esta dissertação, nem a acabar este curso, nem teria tido as oportunidades que tive. A todos, estou eternamente grato. Muito obrigado. v ÍNDICE 3.2 Solução Proposta .................................. 20 3.2.1 Gramática da Linguagem .......................... 21 3.2.2 Arquitetura do Sistema ........................... 25 3.2.3 Álgebra Musical .............................. 27 4 Casos de Estudo 29 4.1 Tocar Música .................................... 29 4.2 Fuga de Duas Vozes ................................. 30 4.3 Definir um teclado ................................. 31 4.4 Teclado de Arpeggios ............................... 32 4.5 Teclado Geral com MIDI ............................... 33 4.6 QWERTY Keyboard ................................. 34 4.7 Conclusão ..................................... 36 5 Desenvolvimento do Interpretador Musikla 37 5.1 Análise Sintática .................................. 37 5.1.1 Descrição Sintática ............................. 38 5.1.2 Gramática ................................. 41 5.1.3 Reconhecedor Sintático ........................... 46 5.1.4 Conversão de PEG para LALR ....................... 46 5.1.5 Tooling ................................... 47 5.2 Semântica Dinâmica ................................ 52 5.2.1 Contexto .................................. 53 5.2.2 Scope de Símbolos ............................. 54 5.2.3 Módulos .................................. 55 5.2.4 Operadores Musicais ............................ 56 5.3 Biblioteca Standard ................................. 61 5.3.1 Inputs & Outputs .............................. 62 5.3.2 Ficheiros de Som .............................. 66 5.3.3 Grelhas .................................. 68 5.3.4 Teclados Musicais ............................. 70 5.3.5 Editor Embutido .............................. 78 5.3.6 Transformadores .............................. 80 5.4 Resumo do Desenvolvimento ............................ 82 6 Guia Rápido de Utilização 85 6.1 Módulos ...................................... 85 6.2 Configuração .................................... 86 6.3 Linha de Comandos ................................. 87 xii ÍNDICE 7 Conclusão 89 7.1 Trabalho Futuro ................................... 90 Bibliografia 93 Apêndices 95 A Gramática PEG 95 B Gramática LALR 103 xiii Lista de Figuras 3.1 Arquitetura Geral do Projeto ............................... 25 3.2 Arquitetura do Interpretador ............................... 26 4.1 Pauta musical gerada pela linguagem, versão áudio disponível aqui5............ 31 5.1 Syntax highlighting da linguagem MusiKLa no editor Visual Studio Code ....... 49 5.2 Captura de ecrã da página de documentação da linguagem ............... 50 5.3 Relatório de erro de parse ............................... 50 5.4 Pauta musical gerada pela linguagem6, versão áudio disponível aqui7........... 57 5.5 Pauta musical gerada pela linguagem, versão áudio disponível aqui8............ 57 5.6 Pauta musical gerada pela linguagem, versão áudio disponível aqui9............ 58 5.7 Pauta musical gerada pela linguagem, versão áudio dísponível aqui10........... 59 5.8 Pauta musical gerada pela linguagem, versão aúdio dísponível aqui11........... 59 5.9 Pauta músical gerada pela linguagem, versão aúdio dísponível aqui12........... 60 5.10 Organização dos módulos standard do projeto ..................... 61 5.11 Representação de duas células de uma grelha ...................... 68 5.12 As várias áreas configuráveis de uma grelha ....................... 69 5.13 Áreas em que a grelha irá mover os eventos, e qual o separador a que pertencem. . . . . 70 5.14 Posição dos eventos após o alinhamento com a grelha ter sido aplicado. ......... 70 5.15 Janela de carregamento dos buffers .......................... 77 5.16 Interface do editor embutido. .............................. 79 5.17 Implementação básica de uma solução de auto-complete................ 79 xv Lista de Tabelas 2.1 Lista de modificadores e exemplos da sua utilização ................... 3 5.1 Lista de abreviaturas possíveis de serem acrescentadas a seguir a uma nota para especificar um acorde. ....................................... 39 5.2 Lista de modificadores e exemplos da sua utilização ................... 40 5.3 Formato nativo suportado pelo FluidSynth ........................ 67 5.4 Linhas de código (sem linhas vazias ou de comentário) do projeto Musikla ........ 83 xvii Listagens 2.1 Exemplo da linguagem alda ............................. 4 2.2 Exemplo da notação ABC .............................. 5 2.3 Exemplo da notação ABC .............................. 6 2.4 Geração de ruído aleatório com volume a metade .................. 6 2.5 Geração de ruído aleatório com um filtro low-pass ................. 7 2.6 Geração de ruído aleatório com um filtro low-pass controlada por uma interface . . . 7 2.7 Declaração de dois canais de aúdio com base em dois osciladores .......... 8 2.8 Dividir um gerador por dois canais de forma desigual ................ 8 2.9 Dividir um gerador por dois canais de forma desigual ................ 8 2.10 Reproduzir um oscilador durante dois segundos ................... 10 2.11 Reproduzir um oscilador infinitamente ........................ 10 2.12 Reproduzir um oscilador, variando a frequência a cada 2 segundos .......... 10 2.13 Exemplos de instruções de avanço no tempo .................... 10 2.14 Exemplos de instruções de avanço no tempo .................... 11 2.15 Reproduzir um sample com valores aleatórios ................... 12 2.16 Reproduzir um notas de uma escala aleatórias, com efeito reverb .......... 12 2.17 Gramática ..................................... 13 2.18 Sistema de Tipos de um ficheiro SoundFont ..................... 15 3.1 Exemplo da sintaxe proposta da linguagem ..................... 21 3.2 Exemplos de notas ................................. 23 3.3 Exemplo da sintaxe de teclados virtuais ....................... 24 3.4 Exemplo da sintaxe proposta da linguagem ..................... 24 4.1 Exemplo da sintaxe para criação de música ..................... 29 4.2 Exemplo da declaração da estrutura e conteúdo de uma simples fuga de duas vozes . 30 4.3 Exemplo da sintaxe para criação de teclados ..................... 31 4.4 Definição de um teclado de acordes ......................... 32 4.5 Exemplo da sintaxe proposta da linguagem ..................... 34 4.6 Exemplo da sintaxe proposta da linguagem ..................... 34 5.1 Expressão Regular que identifica uma nota (quebras de linha adicionadas apenas para claridade de leitura) ................................. 38 xix Listagens 5.2 Exemplos de três definições de acordes possíveis .................. 39 5.3 Produções base da gramática ............................ 41 5.4 Produções base da gramática ............................ 41 5.5 Produções de instruções na gramática ........................ 42 5.6 Produções de expressões na gramática ....................... 43 5.7 Produções de elementos musicais na gramática ................... 44 5.8 Produções associadas a teclados na gramática ................... 45 5.9 Excerto da gramática desenvolvida ......................... 46 5.10 Métodos responsáveis por criarem a AST ...................... 46 5.11 Pequeno excerto da definição da linguagem escrito em Iro .............. 48 5.12 Excerto da música Wet Hands de C418 ....................... 56 5.13 Excerto do começo do tema principal de Westworld, por Ramin Djawadi ....... 57 5.14 Excerto da música Soft to Be Strong de Marina .................. 58 5.15 Exemplo de transposição de um acompanhamento com três notas .......... 58 5.16 Três acordes diferentes arpegiados com o mesmo padrão .............. 59 5.17 Redimensionamento da duração de uma expressão músical ............. 60 5.18 Exemplo de reproduzir um ficheiro a seguir a duas notas ............... 67 5.19 Verificar se um ficheiro de audio está optimizado, e convertê-lo caso contrário . . . . 67 5.20 Código de definição da grelha representada na figura 5.12 .............. 68 5.21 Código alternativo de definição da grelha representada na figura 5.12, com as propriedades left eright ................................. 69 5.22 Exemplo de declaração de duas teclas ........................ 70 5.23 Declaração de três eventos, o primeiro é uma combinação de teclas, o segundo referência o virtual key code, e o terceiro uma nota MIDI ................. 71 5.24 Teclado que imprime as coordenadas do rato sempre que ele se move ........ 72 5.25 Aplicar o modificador hold extend a um teclado inteiro .............. 73 5.26 Código gerado automaticamente para criação do teclado descrito no capitulo anterior . 73 5.27 Declaração de teclado dinâmica recorrendo ao uso de ciclos, condicionais e blocos de código. ....................................... 74 5.28 Gravação de uma performance .......................... 75 5.29 Reprodução de uma performance ......................... 75 5.30 Conversão de uma performance numa sequência de eventos músicais ....... 75 5.31 Instanciação de um buffer ............................. 76 5.32 Funções disponibilizadas para criação de buffers controlados por teclados. ..... 77 5.33 Formato do ficheiro de gravação dos buffers..................... 78 5.34 Abre o editor quando a tecla \ é premida ...................... 78 5.35 Abrir manualmente o editor ............................. 79 5.36 Exemplo de uma função map com versões sincronas e asíncronas .......... 80 xx Listagens 5.37 Implementação da função map usando a nossa abordagem de transformadores . . . 81 5.38 Exemplo de um transformador map e da sua utilização ................ 82 6.1 Processo de instalação do package musikla .................... 85 6.2 Exemplo de um ficheiro de configuração da linguagem ................ 86 6.3 Exemplo de um ficheiro de configuração da linguagem ................ 87 A.1 Gramática PEG da linguagem Musikla ........................ 95 B.1 Gramática LALR da linguagem Musikla ....................... 103 xxi Capítulo 1 Introdução No âmbito das linguagens de programação e engenharia de software, é comum observar como a linguagem, ou o paradigma, de programação escolhido pode afetar de modo profundo as decisões que o programador toma, e por consequência, o produto final. De modo análogo, nós acreditamos que a forma como pensamos e falamos sobre música afeta também a música que criamos, ou a forma como pensamos sobre ela. A linguagem musical mais comum utilizada entre profissionais e entusiastas é certamente a linguagem das pautas musicais. Mas esta linguagem é estática. É como se pudéssemos falar de matemática usando apenas números, mas sem nenhuma linguagem comum para expressarmos conceitos de mais alto nível como equações ou inequações. Era certamente possível representarmos os resultados finais, mas perdia-se a estrutura que possibilitava compreender a um mais alto nível o problema, e até mesmo generalizar a sua solução e encontrar padrões para outros problemas similares. Sendo o processamento de linguagens, em particular de linguagens de programação, uma área de enorme interesse pessoal, surgiu a ideia de como integrar vários desses conceitos na linguagem musical, e desta forna torná-la mais rica e poderosa. O objetivo desta dissertação irá então passar por desenhar e desenvolver uma linguagem que pode ser pensada como uma calculadora musical. Queremos analisar qual a melhor forma de descrever música em formato textual, e de integrar construções comuns na programação, como variáveis, funções, ciclos e estruturas de decisão, entre outros. Esta linguagem deve então servir tanto como uma ferramenta de suporte teórico, para os utilizadores estudarem a estrutura e a teoria de peças musicais, mas também como uma ferramenta prática para praticar a criação de música. Esta última parte prática motivou também a incorporação na nossa linguagem de uma componente de interatividade. Para além de permitir usar pianos, ou o próprio teclado de um computador, para reproduzir as notas habituais, queremos dar ao utilizador a possibilidade de customizar as ações ou os 1 CAPÍTULO 1. INTRODUÇÃO acompanhamentos musicais que tocam em cada tecla. Um sentimento que iremos repetir bastante ao longo desta dissertação é a necessidade deste projeto ser extensível. Isto é, permitir a cada utilizador adicionar novas funcionalidades à linguagem quando precisar, e tornar este processo o mais simples possível, sem fases de recompilação ou necessidade de alterar o código fonte do projeto. Isto porque a produção de música é uma área enorme, e mais importante, é uma forma de expressão criativa. Não temos ilusões de conseguir cobrir sequer a maior parte dos casos de uso de todos os utilizadores. E por isso, providenciar uma base sólida sobre a qual cada pessoa pode facilmente construir, e eventualmente ir juntando essas construções à implementação oficial, parece-nos crítico para o sucesso e para a usabilidade da linguagem. Para realizar isso, iremos analisar no próximo capítulo as linguagens existentes na interseção da programação com a criação de música. Vamos analisar para cada uma, quais os aspetos que não coincidem com aquilo que queríamos encontrar numa linguagem, mas também aquilo que já existe e que está bem feito. E como as boas ideias são para ser aproveitadas, iremos incorporar o que for sensato na nossa linguagem (como por exemplo, usar a sintaxe de descrição de notas e acordes do projeto abc notation1). 1http://abcnotation.com/ 2 Capítulo 2 Estado da Arte Atualmente a produção de música é realizada utilizando programas com interfaces gráficas, geralmente denominados como Digital Audio Workstation (DAW). A minha abordagem irá consistir em estudar formas de criar e tocar músicas ao vivo (e não só) através de uma Domain Specific Language (DSL), usando técnicas inspiradas nas linguagens de programação e no desenvolvimento de software. 2.1 Trabalho Relacionado Existem diversos tipos de linguagens usadas atualmente para produzir ou simplesmente descrever música. Algumas fazem uso do conceito de notas musicais, com recurso a algum sintetizador externo, para gerar os sons, enquanto outras funcionam com base na manipulação direta de ondas de som digitais para criar música. Algumas suportam apenas a descrição estática da música, enquanto outras permitem formas dinâmicas tais como funções, variáveis, estruturas de controlo e repetição, ou até mesmo algoritmos aleatórios que permitem gerar músicas diferentes a cada execução. Language Tipo de Dados Dinâmica Interatividade alda Notação Musical Não N/A abc notation Notação Musical Não N/A Faust Sinais Áudio Sim Botões/Sliders/etc... SuperCollider Sinais Áudio Sim Não ChucK Sinais Áudio Sim Teclados Imperativos SonicPi Notas Sim Não Solução Ideal Notação Musical Sim Teclados Declarativos Tabela 2.1: Lista de modificadores e exemplos da sua utilização 3 CAPÍTULO 2. ESTADO DA ARTE Queremos analisar as opções segundo os seguintes conceitos: Tipo de Dados Refere-se a quais os tipos de dados usados principalmente pela linguagem na componente musical. Sinais de áudio são os de mais baixo nível, enquanto que Notas refere-se à linguagem permitir tocar uma notas manualmente. Notação Musical é a de mais alto nível e permite descrever não só notas, mas acompanhamentos musicais. Dinâmica Corresponde a saber se a linguagem tem construções dinâmicas (como variáveis, ciclos, funções) ou se é apenas estática. Interatividade Algumas linguagens podem permitir interatividade através de interfaces gráficas no computador, como botões e sliders. Outras podem suportar teclados, que podemos dividir em dois tipos: Imperativos, e Declarativos. Imperativos referem-se à possibilidade de ser notificado quando qualquer tecla é premida, uma de cada vez, e decidir manualmente o que fazer. Declarativos referem-se a descrever uma lista de teclas e ações a executar para cada uma delas, sem termos de nos preocupar em receber manualmente os eventos e decidir se o evento se aplica à nossa tecla ou não. Iremos de seguida analisar mais a fundo estas linguagens, bem como comparar as funcionalidades que cada uma oferece relativamente aos conceitos enumerados em cima. 2.1.1 Alda O projeto alda (“alda”, s.d.) é uma linguagem de música textual desenvolvida em JAVA focada na simplicidade: o seu maior ponto de atração é apelar tanto a programadores com pouca experiência musical, bem como a músicos com pouca experiência com programação. Apesar de ser anunciada como direcionada tanto a músicos como a programadores, a linguagem não suporta nenhum tipo de construções dinâmicas, como ciclos ou funções. Este tipo de funcionalidades, se necessário, requer o uso de uma linguagem de programação por cima, que poderia por exemplo, gerar o código alda em runtime através da manipulação de strings antes de o executar. Isto significa que não é possível implementar composições interativas. 2.1.1.1 Exemplos O exemplo seguinte demonstra um simples programa escrito em alda, demonstrando: a seleção de um instrumento (piano:), a definição da oitava base (o3), um acorde com quatro notas (c1/e/g/>c4) em que a última se encontra uma oitava acima das outras. 1piano: o3 c1/e/g/>c4 < b a g | < g+1/b/>e Listagem 2.1: Exemplo da linguagem alda 4 2.1. TRABALHO RELACIONADO É também possível verificar o uso de acidentes (identificados pelos símbolos +ou -a seguir a uma nota) bem como a diferenciação da duração de algumas notas (identificadas pelos números em frente às notas). 2.1.2 ABC Notation A notação ABC (“ABC Notation”, s.d.; Gonzato, 2019) é uma notação textual que permite descrever notação musical. É bastante completa, tendo formas de descrever notas, acordes, acidentes, ligaduras de notas, lyrics, múltiplas vozes, entre outros. Para além das exaustividade de sintaxe que permite descrever quase todo o tipo de música, a popularidade da linguagem também significa que existem já inúmeros conversores de ficheiros ABC para os mais diversos formatos, desde ficheiros MIDI, pautas musicais, ou mesmo ficheiros WAV (gerados através do fornecimento de um ficheiro SoundFont, por exemplo). A complexidade da notação traz tanto vantagens como desvantagens, no entanto: A sua ubiquidade significa que uma maior percentagem de utilizadores já se pode sentir à vontade com a sintaxe, o que não acontece com outras linguagens menos conhecidas. Mas por outro lado, conhecer ou implementar toda a especificação (“ABC Notation Standard v2.1”, 2011) é um feito bastante difícil. No entanto, tal como a linguagem ALDA, as músicas definidas são estáticas, pelo que não serve como uma linguagem de programação de músicas dinâmicas. Ainda assim, apesar de implementar toda a notação ser algo pouco prático, implementar um subset da notação, contendo as construções mais usadas seria uma vantagem enorme que me permitiria aproveitar a familiaridade de muitos utilizadores com as partes mais comuns da sintaxe. 2.1.2.1 Exemplos A sintaxe de um ficheiro ABC é composta por duas partes: um cabeçalho onde são definidas as configurações da música atual, seguido pelo corpo da música. O cabeçalho é formado por uma várias linhas. Cada linha, em ABC chamada de campo, tem uma chave e um valor separados por dois pontos (:). A especificação da notação descreve bastantes campos possíveis, mas os mais usados são: X(número de referência), T(título), M(compasso), L(unidade de duração de nota) e K(armação de clave). 1C, D, E, F,|G, A, B, C|D E F G|A B c d|e f g a|b c' d' e'|f' g' a' b'|] Listagem 2.2: Exemplo da notação ABC No exemplo acima podemos ver uma escala completa das notas (sem acidentes). O chamado C médio é representado por um cminúsculo (a capitalização das letras muda o significado). Para subir uma oitava, podemos anotar as notas com um apóstrofo (c’). As oitavas subsequentes são anotadas por mais apóstrofos. De modo análogo, para baixar uma oitava, devemos usar primeiro a nota em maiúscula (C). As oitavas anteriores são identificadas por uma (ou mais) vírgula a seguir à nota com letra maiúscula (C,). 5 CAPÍTULO 2. ESTADO DA ARTE 1A/2 A/ A A2 __A _A =A ^A ^^A [CEGc] [C2G2] [CE][DF] Listagem 2.3: Exemplo da notação ABC A duração das notas pode ser ajustada relativamente à unidade global definida no cabeçalho acrescentando um número (por exemplo 2) ou fração 1/4 à nota. Os acidentes bemol, bequadro e sustenido podem ser adicionados acrescentando um _,=e^antes da nota, respetivamente. Acordes (notas tocadas ao mesmo tempo) podem ser definidas entre parênteses retos ([e]). A notação disponibiliza muitos mais exemplos de todas as funcionalidades aceites no seu website (“ABC Notation Examples”, 2011). 2.1.3 Faust A linguagem Faust (Orlarey, Fober & Letz, 2009; Orlarey, Graef & Kersten, 2006) é uma linguagem de programação funcional com foco na sintetização de som e processamento de áudio. Ao contrário das linguagens analisadas até agora, não trabalha com abstrações de notas e elementos musicais. Em vez disso, a linguagem trabalha diretamente com ondas sonoras (representadas como streams de números) e através de expressões matemáticas, que de uma forma funcional permitem assim manipular o som produzido. Um dos pontos fortes da linguagem é o facto da sua arquitetura ser construída de raiz para compilar o mesmo código fonte em várias linguagens. De facto, o projeto conta com várias dezenas de targets, desde os mais óbvios (C, C++, Java, JavaScript) até alguns mais especializados (WebAssembly, LLVM Bytecode, instrumentos VST/VSTi). Também permite gerar aplicações standalone para as bibliotecas de audio mais comuns já embutidas (“FAUST Targets”, s.d.). A linguagem vem embutida com uma biblioteca extremamente completa (“FAUST Libraries”, s.d.) que implementa, entre muitas outras, funções de matemática comuns, filtros áudio e funcionalidades extremamente básicas de interfaces gráficas que permitem controlar em tempo real os valores do programa (como botões e sliders, entre outros). 2.1.3.1 Exemplos A documentação do projeto conta com uma quantidade abundante de exemplos (“FAUST Examples”, s.d.) e com um tutorial para iniciantes (“FAUST Quick Start”, s.d.), do qual irei colocar aqui alguns pequenos pedaços de código que demonstram as capacidades fundamentais da linguagem. 1import(”stdfaust.lib”); 2process = no.noise*0.5; Listagem 2.4: Geração de ruído aleatório com volume a metade No primeiro exemplo, podemos ver a estrutura mais básica de um programa escrito em Faust. Na primeira linha é importada a biblioteca standard da linguagem. Na segunda linha podemos ver a keyword 6 2.1. TRABALHO RELACIONADO process, que representa o input eoutput audio do nosso programa. Finalmente, em frente a essa keyword podemos ver a expressão no.noise*0.5 (sendo no onamespace contendo as funções de geração de ruído, e noise correspondendo ao white noise). Isto demonstra a utilização de construções da biblioteca standard, como o gerador de ruído aleatório, bem como a utilização de operadores matemáticos usuais (neste caso a multiplicação) para manipular o áudio, e diminuir o volume para metade. 1import(”stdfaust.lib”); 2ctFreq = 500; 3q = 5; 4gain = 1; 5process = no.noise : fi.resonlp(ctFreq,q,gain); Listagem 2.5: Geração de ruído aleatório com um filtro low-pass Neste exemplo, estamos a usar o operador :para canalizar o output do gerador de ruído para um filtro low-pass, que filtra todas as frequências acima de um valor de corte (a variável ctFreq). Aumentar esta variável resulta num som mais agudo, enquanto que ao diminui-la obtemos um som mais grave (pois o valor de corte é mais baixo, apenas os sons abaixo desse valor são passados). 1import(”stdfaust.lib”); 2ctFreq = hslider(”[0]cutoffFrequency”,500,50,10000,0.01); 3q = hslider(”[1]q”,5,1,30,0.1); 4gain = hslider(”[2]gain”,1,0,1,0.01); 5t = button(”[3]gate”); 6process = no.noise : fi.resonlp(ctFreq,q,gain)*t; Listagem 2.6: Geração de ruído aleatório com um filtro low-pass controlada por uma interface Por fim podemos ver um exemplo igual ao anterior, mas em vez de ter os valores das variáveis estáticos (guardados nas variáveis ctFreq,qegain), estes são controlados em tempo real pela interface definida pelas chamadas à função hslider. Foi também adicionada uma variável tcom um botão ”gate”. Este produz o valor 0 (zero) quando está solto, e o valor 1 (um) quando está pressionado, valor que quando multiplicado pelo resto da expressão serve efetivamente como um on/off switch para todo o sistema. 2.1.4 SuperCollider O projeto SuperCollider (McCartney, 2002; Orlarey et al., 2006) é uma plataforma para geração e sintetização de som e música. É composta em parte pela linguagem interpretada sclang, focada na componente de áudio, mas com funcionalidades de programação generalizada. Também tem um servidor de áudio realtime scsynth, que pode ser controlado pela linguagem sclang, e que implementa diversas técnicas de geração de áudio otimizadas (permitindo ao utilizador programar também as suas próprias técnicas customizadas através de C++). Também integra um IDE scide que disponibiliza um ambiente de edição integrado para todo o ecossistema, bem como ferramentas de ajuda e introdução à plataforma. 7 CAPÍTULO 2. ESTADO DA ARTE É uma linguagem de baixo nível em termos musicais, mas com um grande ecossistema para integrar os mais diversos componentes, desde controladores MIDI a interfaces gráficas. Mas a nível musical, como já referido, foca-se na sintetização e manipulação de ondas de som, sem ter noção de conceitos mais abstratos como notas ou acordes. Tais noções têm de ser manualmente implementadas pelo utilizador, e de uma forma bastante mais verbosa do que o desejável. 2.1.4.1 Exemplos A linguagem do projeto sclang é uma linguagem orientada a objetos mas com aspetos funcionais (como currying ou listas em compreensão). 1{ [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.play; Listagem 2.7: Declaração de dois canais de aúdio com base em dois osciladores No exemplo presente na listagem 2.7, é declarada uma função (demarcada pelo par de chavetas) que retorna uma lista com dois osciladores de ondas sinusoidais. O som dos osciladores é depois reproduzido através da chamada da função play. De notar que como os osciladores estão dentro de um array, isso significa que estamos a gerar múltiplos canais de áudio (dois neste caso), com um oscilador para cada canal. O oscilador SinOsc é apenas um dos geradores de som disponibilizados pelo servidor scsynth, também chamados UGens. Existem outros, e mais importante, é possível compor esses geradores para criar sons mais complexos. Um exemplo ainda relativamente simples desse tipo de composição, presente na listagem 2.8 seria usar o gerador Pan2 que redireciona o som oriundo de outro gerador para dois canais diferentes. A prevalência do som em cada canal (o pan) pode ser costumizada por um segundo argumento, com um valor entre -1 e 1, onde -1 emitiria apenas som no canal da esquerda, 1 emitira apenas som no canal da direita, e qualquer valor pelo meio iria produzir uma gradação entre os dois canais, linearmente proporcional a esse valor. 1{ Pan2.ar(PinkNoise.ar(0.2), -0.3) }.play; Listagem 2.8: Dividir um gerador por dois canais de forma desigual É possível criar uma versão mais interessante deste exemplo quando sabemos que os geradores podem ser utilizados não só para gerar som, mas também para servirem de parâmetros a outros geradores. Por exemplo, na listagem 2.9, em vez de passarmos um número literal −0.3como segundo argumento, podemos passar um oscilador. Desta forma, o som gerado pelo PinkNoise irá variar em proporção ao longo do tempo pelos dois canais gerados, em vez de tocar de forma fixa no mesmo. 1{ Pan2.ar(PinkNoise.ar(0.2), SinOsc.kr(0.5)) }.play; Listagem 2.9: Dividir um gerador por dois canais de forma desigual 8 2.1. TRABALHO RELACIONADO A linguagem é bastante mais complexa, podendo declarar variáveis, objetos, executar funções, estruturas de controlo como condicionais e ciclos, e muito mais. Relativamente à parte musical, contém ferramentas bastante poderosas, mas apenas de baixo nível, com manipulação da música focada em ondas sonoras. 2.1.5 ChucK A linguagem ChucK (Wang, Cook & Salazar, 2015) (Wang, Cook et al., 2003) é outra linguagem de sintetização de áudio digital, similar aos projetos SuperCollider eFaust. O seu maior fator de diferenciação advém da sua abordagem única e interessante de sincronização de processos concorrentes baseado em unidades de tempo (que os autores do projeto denominaram strongly-timed). Este conceito significa que o utilizador pode definir tempos virtuais associados a quando certas instruções devem ocorrer. Tal garantia torna-se útil quando combinada com o conceito de shreds (processos virtuais que podem estar a correr concorrentemente) e que dão a aparência para o utilizador que estão na verdade a correr em paralelo. A máquina virtual por trás da linguagem, responsável por traduzir as instruções em áudio, assegura-se que os shreds são sample-synchronous, ou seja, cada sample (ou amostra) geradas pelos shreds e com o mesmo timestamp virtual irão ser sempre reproduzidas ao mesmo tempo (mesmo que o processador tenha demorado mais tempo a correr um dos shreds do que os outros). Isto é, apesar de as samples poderem-se atrasar, a máquina garante que tal acontece de forma sincronizada, sem a possibilidade de criar desfasamento entre os vários shreds. Mais uma vez esta linguagem lida com o conceito de geração de áudio a um baixo nível, e não é portanto muito adequada para a descrição de notação musical. Apesar disso, este projeto apresenta também um fator diferenciador que permite utilizar a sua componente de agendamento temporal de eventos, para permitir interatividade (através de eventos MIDI ou do teclado do computador). Mais uma vez, mesmo estas funcionalidades são de baixo nível (como iremos abordar mais em detalhe nos exemplos) mas é agradável saber que já vêm pelo menos incluídas com a linguagem. 2.1.5.1 Exemplos A operação central da linguagem ChucK passa pelo operador => (também chamado de operador chucking). A sua semântica pode ser pensada um pouco como a atribuição a variáveis, ou piping de dados. Por exemplo, na listagem 2.10 podemos ver a declaração de um oscilador sinusoidal SinOsc s que é chucked para a variável dac. Esta é uma variável especial da linguagem e que representa o dispositivo de reprodução de áudio (as colunas ou auscultadores). Na linha seguinte podemos ver também que é atribuída à variável now o valor de dois segundos: esta é outra variável especial da linguagem, neste caso responsável por controlar o agendamento da execução de código. Ao executar essa instrução, estamos efetivamente a parar a execução da shred atual durante dois segundos (ficando o som do oscilador definido na linha atrás a enviar som durante os dois segundos para o dispositivo de áudio reproduzir). 9 CAPÍTULO 2. ESTADO DA ARTE 2.4.1 Inicialização Para utilizar a biblioteca FluidSynth, existem três objetos principais que devem ser criados: Settings (fluid_settings_t*), Synth (fluid_synth_t*) e AudioDriver (fluid_audio_driver_t*). O objeto Settings (“FluidSynth Settings”, s.d.) é implementado com recurso a um dicionário. Para cada chave (por exemplo, “audio.driver”) é possível associar um valor do tipo inteiro (int), string (str) ou double (num). Alguns valores podem ser também booleanos (bool), no entanto eles são armazenados como inteiros com os valores aceites sendo apenas 0 e 1. O objeto Synth é utilizado para controlar o sintetizador e produzir os sons. Para isso é possível enviar as mensagens MIDI tais como NoteOn,NoteOff,ProgramChange, entre outros. O terceiro objeto AudioDriver encaminha automaticamente os sons para algum audio output, seja ele colunas no computador ou um ficheiro em disco. Os seguintes outputs são suportados pela biblioteca: Linux: jack, alsa, oss, PulseAudio, portaudio, sdl2, file Windows: jack, PulseAudio, dsound, portaudio, sdl2, file Max OS: jack, PulseAudio, coreaudio, portaudio, sndman, sdl2, file Android: opensles, oboe, file 2.4.2 Utilização Com os objetos necessários inicializados, é necessário ainda especificar qual (ou quais) a(s) SoundFont(s) a utilizar. Para isso podemos chamar o método Synth.LoadSoundFont que recebe dois argumentos: uma string com o caminho em disco do ficheiro SoundFont a carregar, seguido dum booleano que indica se os presets devem ser atualizados para os da nova SoundFont (isto é, atribuir os instrumentos da SoundFont aos canais automaticamente). A função Synth.NoteOn recebe três argumentos: um inteiro a representar o canal, outro inteiro entre 0 e 127 a representar a nota, e finalmente outro inteiro também entre 0 e 127 a representar a velocidade da nota. O canal (channel) representa qual o instrumento que vai reproduzir a nota em questão. Cada canal está atribuído a um programa da SoundFont, e é possível a qualquer momento mudar o programa atribuído a qualquer canal através do método Synth.ProgramChange. Caso se tenha carregado mais do que uma SoundFont, é possível usar o método Synth.ProgramSelect, que permite especificar o id da SoundFont e do banco do instrumento a atribuir. A chave (key) representa a nota a tocar. Sendo este valor um inteiro entre 0 e 127, é necessário saber como mapear as tradicionais notas musicais neste valor. Para isso, basta colocarmos as pich classes e os seus respetivos acidentes sharp numa lista ordenada (C, C#, D, D#, E, F, F#, G, G#, A, A#, B) e associar a eles os inteiros entre 0 e 11 (inclusive). Depois apenas temos de somar a esse número a multiplicação da oitava da nota (a começar em 0) por 12. Podemos deste modo calcular, por exemplo, que a key do C central (C4) é igual a 48 (0+4∗12). Assim, podemos generalizar que para uma oitava 16 2.4. SINTETIZADORES Oe para um tom de nota N, obtemos a chave aplicando a fórmula: N+O∗12 A velocidade (velocity) é também um valor entre 0 e 127. Relacionando a velocidade com um piano físico, esta representa a força (ou velocidade) com que a tecla foi premida. Velocidades maiores geram sons mais altos, enquanto que velocidades mais baixas geram sons mais baixos, permitindo assim ao músico dar ou tirar ênfase a uma nota relativamente às restantes. De notar que um valor igual a zero é o equivalente a invocar o método Synth.NoteOff. A método Synth.NoteOff, por sua vez, recebe apenas dois argumentos (canal e chave), e deve ser chamada passado algum tempo para terminar a nota. Podemos deste modo construir a analogia óbvia que o método NoteOn corresponde a uma tecla de piano ser premida, e NoteOff corresponde a essa tecla ser libertada. 17 Capítulo 3 O Problema e os seus Desafios Daqui para a frente iremos chamar Musikla tanto à linguagem que desenhamos, mas também ao interpretador de referência desenvolvido. Desenhar a nossa linguagem trás consigo os problemas comuns ao desenho de linguagens de programação, bem como desafios novos e únicos relativos ao domínio musical. Alguns desses desafios foram já bastante estudado pela miríade de linguagens de programação, tanto industriais como académicas, que já foram desenvolvidas, pelo que não serão o foco principal deste projeto. Pelo contrário, neste projeto serão focados com mais detalhe os desafios resultantes da integração da componente musical na linguagem. O primeiro desses desafios é a introdução de um novo tipo de dados primitivo não existente na maioria das outras linguagens: Música. Este tipo de dados trás consigo a necessidade implícita de gerir o conceito de tempo na linguagem, tanto na geração de música realtime como offline (em que o tempo a que a música está a ser gerada pode ser mais rápido ou mais lento do que o tempo real). Este conceito de tempo também acaba por escapar para o campo da gramática e da sintaxe da linguagem, necessitando de uma forma de descrição do mesmo que seja flexível, mas não demasiado verbosa ou difícil de ler. Ainda relacionado com o tipo de dados Música, também é importante pensar em como o representar, e os casos que deve cobrir. Para este fim, acho que é importante a linguagem permitir gerar sons potencialmente infinitos. Esta funcionalidade não é tão útil no campo da geração de música offline, mas é extremamente útil quando a música está a ser gerada em tempo real, e possivelmente a ser controlada por um utilizador através do teclado, permitindo começar a tocar música gerada proceduralmente, e deixá-la tocar durante o tempo que for necessário. Como tal é necessário pensar em como a implementação de todo o código depende deste ponto. 3.1 Objetivos Nós podemos assim sumarizar os objetivos principais para a nossa linguagem da seguinte forma: 19 CAPÍTULO 3. O PROBLEMA E OS SEUS DESAFIOS •Declarativa As sequências musicais devem ser descritas de uma forma declarativa (em vez de imperativa). •Dinâmica Introduzir conceitos matemáticos ou de programação, tais como funções e variáveis, para a área musical. •Interatividade Tornar possível criar teclados interativos diretamente a partir da linguagem, e que integrem facilmente com o resto das funcionalidades disponibilizadas. •Lazyness Tornar lazyness a predefinição para as sequências músicais, gerando apenas os eventos estritamente necessários quando são necessários. •Eventos Variados As sequências musicais devem poder descrever extratos musicais complexos, contento notas singulares, pausas, acordes, arpeggios, vozes, e mais. •Múltiplos Inputs Para além de permitir que os extratos musicais sejam descritos na nossa linguagem, também deve ser possível que eles sejam importados ou convertidos de diferentes fontes, como dispositivos ou ficheiros MIDI. •Múltiplos Outputs Guardar ou escrever para diferentes outputs, os eventos que forem gerados pela nossa aplicação. •Extensibilidade Tornar simples o processo de estender e customizar o projeto, sem ser necessário passos como clonar o projeto, recompilar ou modificar o seu código interno. 3.2 Solução Proposta Irá ser desenvolvido um interpretador para a linguagem em Python. A linguagem irá ser extensível, permitindo ao utilizador definir objetos ou funções em Python e expô-los para dentro da linguagem, dando assim acesso à grande quantidade de módulos já existentes para os mais variados fins. Como exemplo da extensibilidade da linguagem, irá também ser desenvolvido por cima dela uma biblioteca de construção de teclados virtuais que permitem associar a eventos de teclas notas ou sequências musicais, ou mesmo instruções a serem executadas na própria linguagem. Para resolver o problema da representação do tempo, toda a linguagem irá ter noção implícita desse conceito, mesmo que apenas algumas construções o utilizem. Isto significa que durante toda a execução, haverá uma variável de contexto que será implicitamente passada para todas instruções e todas as chamadas de funções que, entre outras coisas, irá manter registo da passagem do tempo. Desta forma os construtores que precisarem do contexto, como por exemplo a emissão de notas musicais, podem aceder ao tempo atual bem como modificá-lo. A existência deste contexto implícito significa que as funções Python não podem ser expostas diretamente para a linguagem, mas graças à expressividade do Python é possível construir uma Foreign 20 3.2. SOLUÇÃO PROPOSTA Function Interface que seja simples de usar e que evite que o utilizador tenha de mapear as funções manualmente. Em vez disso, pode simplesmente marcá-las como sendo context-free (funções que não têm noção da existência do contexto implícito), e elas serão então tratadas de forma apropriada. O tipo de dados Música irá ser implementado sobre o conceito de iteradores (e mais especificamente geradores) fornecido pelo Python para tornar a criação de música lazy. No entanto, este paradigma deve ser completamente opaco para o utilizador da linguagem: a decisão de usar o modelo de execução normal, ou funções geradores deve ser tomado em segundo plano pelo motor de execução da linguagem, sempre que este for necessário. Isto é, ao contrário da maioria das linguagens que exigem para a utilização de geradores que o utilizador declare explicitamente que quer ”emitir”um valor através de alguma keyword, geralmente yield, na nossa linguagem sempre que alguma função produzir um valor do tipo de música que não seja consumido de alguma forma (atribuído a uma variável ou passado a uma função, por exemplo), esse valor musical é implicitamente emitido para o gerador, uma vez que esse caso será o mais comum. Para evitar que o valor seja emitido, é necessário descartá-lo manualmente onde for caso disso. Se por outro lado a função lidar apenas com valores não-musicais, a sua execução irá seguir o modelo tradicional (onde a função termina a sua execução antes de retornar o controlo ao local onde foi chamada). 3.2.1 Gramática da Linguagem A gramática completa da linguagem pode ser vista no Anexo A. Mas antes de abordarmos em mais detalhe como irá der desenhada a gramática da linguagem, podemos abordar dois pequenos exemplos que demonstram a geração de notas musicais. 1V70 L1 T120; 2 3fun melody () { 4V120; 5 6r/4 ^g/4 ^g/4 ^g/4; 7^f/2 e/8 ^d3/8; 8^c2; 9} 10 11 fun accomp () { 12 V50; sustainoff(); 13 14 ^Cm; 15 BM; 16 AM; 17 } 18 19 # Create the notes from a melody with a piano and accompany it with a violin in parallel 21 CAPÍTULO 3. O PROBLEMA E OS SEUS DESAFIOS 20 $notes = :piano melody() | :violin accomp(); 21 22 # Play the notes twice 23 play( $notes * 2 ); Listagem 3.1: Exemplo da sintaxe proposta da linguagem O desenho da gramática da linguagem é composto por três partes, todas elas interligadas entre si. Instruções e Declarações Similar a quase todas as linguagens de programação, esta parte cobre a declaração de funções, variáveis, operadores e expressões em geral. Extratos Musicais Um tipo de expressões especial, que em vez de produzir números ou strings literais, reconhece expressões musicais (notas, acordes, etc). Teclados Virtuais Açúcar sintático para facilitar a declaração de teclados virtuais. Para além de alguns construtores próprios, faz uso das expressões gerais e de extratos musicais descritas acima. 3.2.1.1 Instruções e Expressões As instruções e expressões da gramática são baseadas nas linguagens como C e JavaScript. Chavetas são usadas para delinear blocos de código. As variáveis são prefixadas com um dólar ($) para prevenir ambiguidades com notas musicais. Cada instrução é separada com um ponto e vírgula (;) a menos que sejam blocos de código que terminem com o fechar de chavetas. Os parâmetros de funções são separados por vírgulas, mas como as vírgulas também são usadas para indicar a descida de oitava nas notas, o ponto e vírgula (;) pode ser usado nesses casos para prevenir ambiguidades. Em termos de instruções suportadas, a linguagem irá ter as usuais: Declaração de Funções fun function_name ( $arg1, $arg2, <...> ) { } Ciclos While while (<condition>) { } Ciclos For for ($var in <expr>) { } Condicionais If if (<condition>) { } else { } Atribuições a Variáveis $var = <expr>; Chamada de Funções function_name( <arg1>, <arg2>, <...>, named_arg = <arg_n> ); 3.2.1.2 Extratos Musicais A gramática de expressões ou extratos musicais tem como base fundamental os seguintes blocos: notas, pausas e modificadores. As notas são identificadas pelas letras A até G, seguindo a notação de Helmholtz(“Helmholtz Pitch Notation”, s.d.) para denotar as respetivas oitavas. Podem também ser seguidas de um número ou de uma fração, indicando a duração da nota. 22 3.2. SOLUÇÃO PROPOSTA 1C,, C, C c c' c'' c''' c'/4 A1/4 B2 Listagem 3.2: Exemplos de notas As notas podem depois ser compostas sequencialmente (como demonstrado em cima, em que cada nota avança o tempo pelo valor da sua duração) ou em paralelo (separados por uma barra vertical |, criando uma bifurcação do contexto em dois, que irão correr em paralelo). Devemos notar que o operador paralelo tem a menor precedência de todos, pelo que não é necessário agrupar as notas com parênteses quando se usa. Isto é, as duas expressões seguintes são equivalentes. 1ABC|DEF 2(ABC)|(DEF) É também possível agrupar estes blocos com recurso a parênteses. Os grupos herdam o contexto da expressão superior, mas as modificações ao seu contexto permanecem locais. Isto permite, por exemplo, modificar configurações para apenas um conjunto restrito de notas. No exemplo seguinte, a velocidade da nota Cé 70, mas para o grupo de notas A B a velocidade é 127. 1v70 (v127 A B) C Os modificadores disponíveis são: Velocity A velocidade das notas, tendo o formato [vV][0-9]+. Duração A duração das notas, tendo o formato [lL][0-9]+ ou [lL][0-9]+/[0-9]+. Tempo O número de batidas por minuto (BPM) que definem a velocidade a que as notas são tocadas, tendo o formato [tT][0-9]+. Compasso Define o compasso, que descreve o tipo de batida da música e o comprimento de uma barra na pauta musical. Tem o formato [sS][0-9]+/[0-9]+. Oitava Permite mudar qual a oitava base (por predefinição 4). Tem o formato [oO][0-9]. Instrumento Qual o identificador numérico do instrumento (a começar em um) a utilizar (regra geral, seguindo o standard General MIDI). Segue o formato [iI][0-9]+. É também possível definir qual o instrumento a ser utilizado para as notas. Todas as notas pertencentes ao mesmo contexto depois do modificador utilizarão esse instrumento. 1(:cello A F | :violin A D) Para além destas funcionalidades, também existe algum açúcar sintático para algumas das tarefas mais comuns na construção de acompanhamentos, como tocar acordes ou repetir padrões. 1( [BG]*2 [B2G2] )*3 23 CAPÍTULO 3. O PROBLEMA E OS SEUS DESAFIOS 3.2.1.3 Teclados Virtuais Para além de permitir declarar extratos musicais para serem tocados automaticamente, a linguagem permite declarar teclados virtuais. Associados às teclas do teclado podem estar notas, acordes, melodias, ou qualquer outro tipo de expressão suportado pela linguagem. 1# Now an example of the keyboard. We can declare variables to hold state 2$octave = 0; 3 4$keyboard = @keyboard hold extend (v120) { 5# Keys can be declared to play notes 6a: C; s: D; d: E; f: F; g: G; h: A; j: B; 7# Or chords 81: [^Cm]; 2: [BM]; 3: [AM]; 9 10 # Or any other expression, including code blocks that change the state 11 z: { $octave -= 1; }; 12 x: { $octave += 1; }; 13 } 14 15 # The set_transform function allows changing all events before they are emitted by this ,→keyboard 16 $keyboard::map( fun ( $k, $events ) => $events + interval( octave = $octave ) ); Listagem 3.3: Exemplo da sintaxe de teclados virtuais Cada teclado aceita opcionalmente uma lista de modificadores, tais como: hold Começa a tocar quando a tecla é premida e acaba de tocar quando é solta toggle Começa a tocar quando a tecla é premida, e acaba de tocar quando ela é premida novamente repeat Quando a nota/acorde/música fornecida acaba de tocar, repete-a indefinidamente extend Em vez de tocar as notas de acordo com a sua duração, estende-as todas até a tecla ser levantada/premida novamente (quando usado em conjunto com hold ou toggle, respetivamente) Também é possível passar uma expressão opcional entre parênteses que irá ser aplicada a todas as notas do teclado (no exemplo acima especificando um instrumento e o volume das notas com (:violin v120)), evitando assim ter de a repetir em vários sítios. Para além disso, será possível controlar muitos mais aspetos do teclado atribuindo-o a uma variável e chamando funções a partir daí, como por exemplo efetuar transformações nos eventos e notas emitidos, ou simular o carregar e soltar das teclas. 1# The set_transform function allows changing all events before they are emitted by this ,→keyboard 24 3.2. SOLUÇÃO PROPOSTA 2$keyboard::map( $events => transpose( $events, octave = $octave ) ); 3# It is possible to synchronize the keyboard with a grid to align the timings of key presses 4# and releases to said grid 5$keyboard::with_grid( Grid::new( 1, 4 ) ); 6# We can also simulate key presses and releases programmatically 7$keyboard::start( ”ctrl+z” ); 8$keyboard::stop_all(); Listagem 3.4: Exemplo da sintaxe proposta da linguagem 3.2.2 Arquitetura do Sistema O sistema irá ser composto por um interpretador de linguagem desenvolvido em Python, acompanhado por uma interface de linha de comandos que faça uso do interpretador. Figura 3.1: Arquitetura Geral do Projeto O interpretador receberá um script obrigatório como input. Esse input poderá depois determinar quais os inputs (opcionais) que irá usar, como o teclado do computador, ficheiros ou teclados MIDI, ficheiros de performance (gravações de reproduções anteriores consistindo nos eventos em que as teclas foram premidas). Os eventos gerados pelo script e os restantes inputs serão depois redirecionados para os diversos outputs, que podem ser as colunas do dispositivo, ficheiros WAV ou MIDI, ficheiros em formato PDF com a pauta musical, entre outros. Toda a linguagem irá ser desenvolvida com extensibilidade em mente nos seguintes pontos: 25 CAPÍTULO 4. CASOS DE ESTUDO A sequência de teclas a ser premida neste exemplo é: A5554S321DSA567D7 76S698967. Os tempos de cada tecla são relativamente uniformes e podem ser facilmente deduzidos ouvindo a versão sonora deste extrato musical, disponível aqui2. No início deste exemplo podemos ver a declaração de uma função toggle_sustain, que recebe como parâmetro uma variável por referência. O que isto significa é que qualquer alteração ao valor da variável dentro desta função, reflete-se também na variável que for passada à função quando esta é chamada. Lá dentro fazemos uso da função cc que permite controlar diversos controlos MIDI. Neste caso, o controlo 64 refere-se ao pedal de sustain de um piano (que deixa as notas a tocar durante mais algum tempo mesmo depois da sua tecla ser levantada). O valor 0 (zero) que lhe é passado significa desligar esse pedal, e o valor 127 significa ligar esse pedal. No futuro, apesar de ser sempre possível recorrer a este tipo de funções de baixo nível, irão ser adicionadas à biblioteca standard as funções mais comuns (como por exemplo, sustainoff() esustainon()). Depois podemos ver a declaração de dois teclados virtuais (a linguagem permite mais do que um teclado ativo ao mesmo tempo). O primeiro mapeia a algumas teclas (a,s,d,feg) o conjunto de acordes usados nesta música. Para além disso também define alguns modificares a serem usados por este teclado (cujo significado é discutido na sub-secção 3.2.1.3). O segundo teclado funciona de forma similar, atribuindo às teclas de 1 a 9 notas individuais a serem tocadas. Neste teclado podemos também ver que notas musicais não são os únicos elementos que podem ser associados a teclas. Também é possível descrever expressões arbitrárias (como neste caso, a chamada da função toggle_sustain( $sustained ) associada à tecla c). Outro ponto a notar sobre o segundo teclado é a declaração entre parênteses (V120) que permite modificar o volume das notas tocadas por este teclado (que se sobrepõe ao volume global V70 indicado no início do código). Isto é uma forma simples de prefixar configurações a todas as notas do teclado, evitando ter de copiar essas configurações para todas as notas. 4.4 Teclado de Arpeggios Neste caso vamos construir um teclado mais dinâmico. Vamos associar a algumas teclas diferentes acordes. Mas em vez de tocar simplesmente esses acordes, vamos tocá-los como arpeggios aplicados a um padrão. É importante notar que o padrão não está escrito estaticamente, mas sim guardado na variável $pat. Isto significa que podemos depois usar um segundo keyboard para controlar qual o padrão ativo. E para tornar o exemplo ainda mais divertido, usamos as setas up edown para aumentar ou diminuir a transposição das notas resultantes, dando assim mais variedade aos quatro acordes definidos. É fácil de ver também como este teclado é extremamente versátil, e pode ser facilmente adaptado para um qualquer número de acordes e arpeggios que se queira utilizar. 2Soft to Be Strong https://drive.google.com/file/d/1ncy4vlCbbiGQ14lurfsU4LCCUBK0ve13/view?usp=sharing 32 4.5. TECLADO GERAL COM MIDI 1S4/4 L/4 T132 V120; 2 3$patterns = @[ 4CDED2EC2; 5C r d' e2 r c2; 6]; 7 8$t = 0; $pat = $patterns::[ 0 ]; 9 10 @keyboard { 11 a: [CM]; 12 s: [Am]; 13 d: [FM]; 14 f: [Dm]; 15 }::map(fun($k, $m) => $m * $pat + $t); 16 17 @keyboard { 18 1: { $pat = $patterns::[ 0 ] }; 19 2: { $pat = $patterns::[ 1 ] }; 20 21 up: { $t += 1 }; 22 down: { $t -= 1 }; 23 }; Listagem 4.4: Definição de um teclado de acordes 4.5 Teclado Geral com MIDI Os dois teclados vistos até agora são interessantes quando temos um sub-conjunto específico de notas e acordes com os quais queremos tocar. Mas às vezes é interessante ter uma ferramenta mais generalizada para experimentar. Este exemplo faz uso de funções disponibilizadas na biblioteca standard da linguagem para fazer isso mesmo. •keyboard\piano() Cria um teclado a simular um piano com as teclas do computador. A primeira linha de teclas corresponde às teclas brancas, a segunda às teclas pretas e a terceira novamente às brancas mas uma oitava acima. •keyboard\midi() Cria um teclado que está à escuta dos eventos MIDI (possivelmente de um piano externo ligado ao computador). • De seguida também vemos que é possível fazermos override a algumas teclas se quisermos algo mais específico. Isto é opcional e pode ser ignorado se quisermos. 33 CAPÍTULO 4. CASOS DE ESTUDO •keyboard\bufpad() Aqui criamos um teclado que está à escuta das teclas numéricas (de 1 até 9) e cria um buffer em cada uma delas. Podemos usar esses buffers para guardar em memória melodias que toquemos com os teclados, para depois as reproduzir quando quisermos. •keyboard\repl() Finalmente criamos um último teclado que faz a tecla ”\”ativar o editor embutido. Assim conseguimos alterar o nosso programa enquanto ele está a correr, alterar teclas, mexer nos buffers, declarar variáveis ou gravar os resultados das nossas experiências para disco. 1# Use a ”standard” keyboard piano 2$piano = keyboard\piano(); 3# Maybe even add a MIDI piano? 4$piano += keyboard\midi(); 5# Or declare a custom piano 6$piano += @keyboard hold extend { 7a: [^Cm]; s: [BM]; d: [AM]; 8f: [EM]; g: [^Fm]; 9}; 10 11 # Creates a keyboard that declares a set of buffers. By default, 12 # the numpad keys are used (creating 10 available buffer slots) 13 # Save key is F8, Load key is F7 14 $piano += keyboard\bufpad(); 15 # Creates a keyboard that associates the interpreter accessible by the ”\” key 16 $piano += keyboard\repl(); Listagem 4.5: Exemplo da sintaxe proposta da linguagem 4.6 QWERTY Keyboard No exemplo anterior utilizamos as funções fornecidas pela biblioteca standard. Aqui vamos ver um exemplo de como poderíamos programar as nossas próprias funções, utilizando funcionalidades mais dinâmicas (como ciclos for) do que simplesmente declarar cada tecla manualmente. Neste caso estamos a criar um teclado em que cada linha de teclas começa uma oitava acima de onde a linha de baixo começou, e as teclas na mesma linha sobem um semitom cada uma. 1fun qwertyboard () { 2# Small shortcut function 3fun ivl ($o, $s) => interval( octaves = $o, semitones = $i ); 4 5# Maps the lines on a keyboard to semitone offsets to List[ List[ str ] ] 6$lines = @[ 7”qwertyuiop”::split(), 8”asdfghjkl”::split(), 34 4.6. QWERTY KEYBOARD 9”zxcvbnm,.”::split() 10 ]; 11 12 $octave = 0; 13 $semitone = 0; 14 15 $keyboard = @keyboard hold extend { 16 for ( $oct, $chars in enumerate( $lines ) ) { 17 for ( $sem, $c in enumerate( $chars ) ) { 18 [ $c ]: c + ivl( -$oct, $sem ); 19 }; 20 }; 21 }::map( fun( $k, $events ) => $events + ivl( $octave, $semitone ) ); 22 23 $keyboard += @keyboard { 24 up: { $octave += 1 }; 25 down: { $octave -= 1 }; 26 right: { $semitone += 1 }; 27 left: { $semitone -= 1 }; 28 }; 29 30 return $keyboard; 31 } Listagem 4.6: Exemplo da sintaxe proposta da linguagem A primeira parte da função declara um array com as três linhas de caracteres presentes num teclado QUERTY. Isto permite-nos ao declarar as teclas no keyboard, e fazê-lo de forma dinâmica (ao invés de associar a cada tecla uma nota manualmente). Essa declaração, inspirada nas listas por compreensão do Python, funciona através de uma construção similar a um ciclo for. A variável $c corresponde a cada item da linha (neste caso, casa tecla), a variável $sem permite-nos obter o índice da letra atual na linha. A cada letra é associada a nota c transposta pelo índice da tecla e da linha a que pertence. Para além disso também podemos ver a declaração de mais quatro teclas (correspondentes às quatro setas do teclado) que permitem deslocar as notas tocadas por oitavas completas ou por semitons. Para isso, estas teclas têm associadas uma expressão de bloco (identificada pelas chavetas {e}). Lá dentro é possível meter uma instrução (ou opcionalmente várias, separadas por pontos e vírgulas ;). O valor da última expressão é o valor de retorno da expressão de bloco toda, pelo que seria possível que uma tecla fizesse mais que uma coisa (alterar o estado e no fim retornar ainda notas para serem tocadas, por exemplo). Finalmente vemos também um exemplo do método map, um dos vários métodos que o objeto Keyboard disponibiliza e que permitem modificar ainda mais o comportamento dos teclados, alterando o valor emitido por cada tecla de acordo com a função passada. 35 CAPÍTULO 4. CASOS DE ESTUDO 4.7 Conclusão Com estes exemplos pudemos ver como ter uma sintaxe declarativa torna bastante compacta a descrição de acompanhamentos musicais. Mas mais do que poder descrevê-los estaticamente, o facto de podermos repetir e transformar sequências musicais aumenta ainda mais a produtividade do utilizador. Para além disso também conseguimos observar que a integração dos teclados abre portas para situações bastante interessantes, principalmente quando usados em conjunto com as ferramentas de programação dinâmica disponíveis. Um exemplo disso é o caso do nosso teclado de arpeggios, onde podemos definir apenas um conjunto de acordes, e depois aplicar uma transformação comum a todos eles para produzirem expressões mais complexas (que podem ser depender de variáveis que vão mudando ao longo da execução). Para além disso, também concluímos que é importante que a biblioteca standard inclua ferramentas genéricas e prontas a utilizar, tais como teclados com as teclas já preenchidas para os casos de uso mais comuns. Isto é útil para facilitar a utilização da linguagem por pessoas que possam sentir-se à vontade com pequenos scripts que mudem algumas variáveis e chamem algumas funções, mas não tenham experiência suficiente para criar tudo de raiz. Por fim, é também algo importante permitir a execução de código (nem que seja apenas de expressões ou instruções singulares, uma de cada vez) em runtime. Isto permite ir refinando o programa, principalmente os teclados que possam estar ativos, sem ser necessário terminar a execução do programa. Tal ação, para além de ser mais incomodativo e diminuir a capacidade do utilizador, também apresenta a desvantagem de fazer a aplicação perder a memória, e como tal o estado das variáveis e dos buffers que pudessem estar a ser usados. 36 Capítulo 5 Desenvolvimento do Interpretador Musikla O desenvolvimento do projeto pode ser dividido de grosso modo em três camadas. Nelas são cobertos um grupo abrangente de aspetos tanto da área do processamento de linguagens e do desenvolvimento de DSL’s, da teoria musical, e do processamento digital de áudio. Na camada da linguagem esteve mais proeminente a área de processamento de linguagens, por motivos óbvios. Mas nas decisões tomadas durante o desenvolvimento desta camada, estiveram sempre presentes também as necessidades específicas que a teoria musical (e a sua notação) impõem numa linguagem de programação. Do mesmo modo, o interpretador faz claramente uso de tópicos do domínio do processamento de linguagens, mas é ainda mais fortemente influenciado pelas restrições e requisitos impostos pela componente musical da linguagem. Esta influencia a forma e a semântica da execução dos vários operadores disponibilizados. A última camada, de desenvolvimento de uma biblioteca, afeta composta pelos objetos e procedimentos que têm como objetivo facilitar a utilização da linguagem. Para isso foi necessário identificar os casos de utilização mais comuns e prioritários, de modo a guiar a construção destas interfaces para refletirem uma utilização real da linguagem e da aplicação. 5.1 Análise Sintática A camada sintática da aplicação pode ser conceptualmente dividida em duas fases: Sintaxe Esta fase caracteriza-se por delinear qual a sintaxe usada pela linguagem, bem como os construtores e operadores suportados; Parser Nesta fase foi desenvolvido um parser em Python, responsável por converter o código fonte da linguagem numa Abstract Syntax Tree (AST); 37 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA No entanto, a realidade é que a abordagem seguida (não só nesta camada mas como em todo o projeto) foi mais iterativa, dividindo cada fase em porções semi-independentes e intercalando as várias porções das diversas fases. Esta abordagem tem a vantagem de permitir ir testando e experimentando com o projeto mais cedo do que seria possível com um modelo de desenvolvimento em cascata. 5.1.1 Descrição Sintática A sintaxe da linguagem é fortemente inspirada nas usualmente chamadas linguagens da família C, com recurso a parênteses curvos e chavetas para delínear os vários blocos da linguagem. No entanto, as expressões são complementadas com um novo conjunto de literais e operadores dedicados à componente músical da linguagem. Conseguir juntar estes dois mundos traz consigo alguns desafios que serão discutidos mais em detalhe em cada umas das secções seguintes. 5.1.1.1 Constantes ou Literais Literais referem-se ao conceito de sintaxe desenhada com o propósito de descrever dados (literais) no código. São usados em quase todas as linguagens de programação (e na nossa também) para descrever números, strings e booleanos. A maior diferença nesta área entre a nossa linguagem e as restantes, foi a adição de literais responsáveis por modelar conceitos musicais, como notas, pausas e acordes. Esta sintaxe, tal como já foi mencionado anteriormente, foi inspirada pelo projeto abc notation, com algumas modificações. 5.1.1.2 Notas e Pausas A sintaxe de notas descrição de notas é composta por quatro componentes: acidentes,pitch (obrigatório), oitava eduração. 1[_^]* 2[a-gA-G] 3[',]* 4([0-9]*\/)?[0-9]* Listagem 5.1: Expressão Regular que identifica uma nota (quebras de linha adicionadas apenas para claridade de leitura) Opitch refere-se à nota (ou frequência) que deve ser tocada. O C médio (também conhecido como C4) é descrito simplesmente como C. É possível descer uma ou mais oitavas acrescentando uma ou mais vírgulas ,. Para subir uma oitava, podemos primeiro substituir as letras maiúsculas por minúsculas. Para subir ainda mais oitavas, podemos acrescentar uma ou mais pelicas ’. Para subir ou descer semitons, podemos prefixar as notas com os acidentes ^e_, respetivamente. As pausas são mais simples, sendo compostas simplesmente pela letra rseguida da sua duração (usando as mesma sintaxe das notas). 38 5.1. ANÁLISE SINTÁTICA 5.1.1.3 Acordes Para definir acordes na linguagem, colocam-se várias notas dentro de parênteses retos. A notação usada para cada nota inclui os seus três primeiros componentes (acidentes, pitch e oitava), mas exclui a duração. Em vez de definir a duração em cada nota, esta é definida globalmente no acorde após fechar os parênteses retos. Por conveniência, para evitar ao utilizador ter de introduzir todas as notas de um acorde manualmente, temos uma sintaxe abreviada para os tipos de acordes mais comuns, onde é apenas necessário introduzir a nota base seguido do tipo de acorde. Esta sintaxe irá suportar mais tipos no futuro, sendo atualmente possível usar os seguintes sufixos: Abreviaturas Tríades M, m, aug, dim, + Quinta 5 Sétimas m7, M7, dom7, 7, m7b5, dim7, mM7 Tabela 5.1: Lista de abreviaturas possíveis de serem acrescentadas a seguir a uma nota para especificar um acorde. A decisão de envolver cada acorde com parênteses retos deveu-se ao facto de muitas abreviaturas serem já populares no domínio da notação musical, e como tal o ideal era não as mudar. No entanto, algumas dessas abreviaturas poderiam entrar em conflito com outros componentes da declaração da nota. Por exemplo, C7 poderia ser tanto um acorde de sétima como uma nota com duração de 7. Com a separação por parênteses retos, a ambiguidade deixa de existir, sendo óbvio que [C7] é um acorde de sétima, e C7 é uma nota com duração 7. 1[CFG]/4 [^Fm] [C5]2 Listagem 5.2: Exemplos de três definições de acordes possíveis 5.1.1.4 Modificadores de Voz Para além de permitir descrever notas, também é possível ter modificadores de voz que permitem alterar certas propriedades das notas e acordes. Duas destas propriedades (duração e oitava) podem ser depois customizadas em cada nota ou acorde, como já vimos. No entanto, em vez de estes valores substituírem simplesmente os valores predefinidos, eles “complementam-se”. Ou seja, se declararmos por exemplo que a duração base das notas é 1 4. Quando definirmos alguma nota a seguir com a duração de 1 2, a sua duração real irá ser calculada da seguinte forma 1 4×1 2=1 8. Do mesmo modo, quando definimos por exemplo a oitava base como 5(o valor predefinido é 4), a nota C, passa a representar a oitava 5−1=4(por predefinição seria 4−1=3) (nota que aqui estamos a falar de oitavas indexadas a zero. No mundo da música elas são geralmente indexadas a um). 39 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA Podemos então ver a lista dos modificadores aceites pela linguagem, bem como exemplos de utilização e os seus respetivos valores predefinidos (usados quando nenhum modificador desse tipo é aplicado). Nome Modificador Exemplo Predefinição Instrumento INI46 I1 Velocidade VNV100 V127 Tempo TNT120 T60 Duração LN L/N LD/N L2 L/4 L3/8 L1 Oitava ONO2 O4 Compasso SD/NS3/4 S4/4 Tabela 5.2: Lista de modificadores e exemplos da sua utilização 5.1.1.5 Variáveis e Funções Uma das consequências da introdução da sintaxe de notas literais foi a impossibilidade de ter variáveis com certos nomes. Uma variável chamada a, por exemplo, iria entrar em conflito com a nota do mesmo nome. Do mesmo modo, uma variável chamada i1 iria entrar em conflito com o modificador de instrumento. Em vez de criar casos de exceção para as variáveis que possam ter nomes que conflitam com outros construtores sintáticos, seguimos o exemplo de outras linguagens como PHP,Perl ou Powershell, e decidimos prefixar as nossas variáveis com o carácter $. No caso das funções foi possível evitar a ambiguidade (e do mesmo modo a obrigatoriedade de as prefixar com um carácter) devido ao facto de as funções terem obrigatoriamente um par de parênteses (sem espaço entre os mesmos e o nome da função) quando são chamadas. No entanto não quer dizer que as funções passaram imunes à introdução das literais de música. Uma vez que a vírgula é usada para mudar a oitava de uma nota (e foi escolhida de forma a manter compatibilidade com a sintaxe do projeto abc notation), existem casos em que esta não pode ser utilizada para separar os argumentos passados a uma função. Por exemplo, dada a expressão function_name(C, A, $a, 2), quantos argumentos podemos concluir que a mesma tem? Quatro? A resposta correta seria dois, pois as duas primeiras vírgulas poderiam pertencer à nota ou ao separador da função. Mas neste caso a nota teria prioridade na nossa gramática, pelo que C, A, $a seria o primeiro argumento, e 2seria o segundo. A solução tomada inicialmente foi utilizar ponto-e-vírgula para substituir a vírgula como separador de argumentos nas funções. E enquanto isso resolveu os problemas de ambiguidade, tornou-se óbvio à medida que que a linguagem foi avançando, que a prevalência da vírgula como separador de argumentos em quase todas as linguagens de programação mais populares fazia com que houvesse um custo mental de mudança de contexto sempre que alguém mudava de alguma linguagem para a nossa, e vice-versa. 40 5.1. ANÁLISE SINTÁTICA A solução escolhida no final foi um compromisso entre as duas opções: tanto a vírgula como o pontoe-vírgula podem ser usados como separadores de argumentos, com a exceção de quando o argumento é uma nota musical, onde o ponto-e-vírgula tem de ser obrigatoriamente usado. Assim sendo, poderíamos rescrever o exemplo anterior da seguinte forma function_name(C; A; $a, 2), passando a função a receber agora os quatro argumentos como seria inicialmente esperado. 5.1.2 Gramática Aqui vamos analisar brevemente uma versão simplificada da gramática (a versão completa usada pela aplicação pode ser vista no Apêndice A). Nesta versão simplificada, incluída para facilitar a leitura, não aborda detalhes como recursividade à esquerda, ou precedência de operadores. Obviamente, na gramática real foram usadas as técnicas usuais para cobrir esses casos. A notação da linguagem Musikla cobre várias sub-gramáticas, tanto da parte musícal, descrição de teclados mas também instruções e expressões de uma programação de linguagem genérica, que iremos abordar de seguida. 5.1.2.1 Corpo Comecemos pela descrição geral do corpo de um ficheiro Musikla. O corpo é composto por uma lista de statements (ou instruções) que são obrigatoriamente separados por pontos e vírgula (sendo apenas o último opcional). No fim de todas as instruções, opcionalmente, o utilizador também pode inserir um bloco de código Python (que é denotado pelo prefixo @python e consome todo o texto até ao fim). 1main <- body python? EOF 2 3body <- statement ( ”;” statement )* ”;”? 4 5python <- ”@python” python_body 6 7python_body <- ( r”.*” r”\r?\n”? )* Listagem 5.3: Produções base da gramática Ao longo da gramática são também usadas produções triviais, como inteiros, strings, booleanos entre outras. Sendo tão comuns e simples, não vale a pena estar a descrevê-las todas. Deixamos no entanto aqui duas produções que vão ser usadas, mas cujas definições podem ser ligeiramente diferentes do usual. 1sep = r”[,;]” 2 3identifier <- r”[a-zA-Z\_]” r”[a-zA-Z0-9\_\\]”* Listagem 5.4: Produções base da gramática 41 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA 5.1.5.1 Syntax Highlighting Para oferecer syntax highlighting para a nossa linguagem, que a nosso ver torna a experiência de programar com a mesma muito mais agradável, utilizamos uma linguagem chamada Iro3. Esta linguagem é uma DSL que permite descrever a sintaxe através de expressões regulares, e associar estilos a cada uma. 1:pattern { 2regex \= (=>|<=|>=|==|!=|[=\*\|\+\-\/]|>=) 3styles [] = .operator; 4} 5 6: pattern { 7regex \= (#.*) 8styles [] = .comment; 9} Listagem 5.11: Pequeno excerto da definição da linguagem escrito em Iro A sua grande vantagem é permitir, a partir de uma única definição, gerar versões em formatos específicos suportados por vários editores, como por exemplo o TextMate,Visual Studio Code,Atom, Sublime 3, ou até convenientemente para o formato suportado pelo popular módulo Python Pygments. Usamos este módulo para colorir os exemplos de código na nossa documentação, como iremos ver na secção seguinte. 3https://eeyo.io/iro/ 48 5.1. ANÁLISE SINTÁTICA Figura 5.1: Syntax highlighting da linguagem MusiKLa no editor Visual Studio Code 5.1.5.2 Documentação Também foi escrita documentação para a linguagem disponível online4, que inclui informação sobre a sintaxe, exemplos de scripts funcionais escritos em musikla, e uma API com a descrição das várias funções disponibilizadas, as suas assinaturas e alguns exemplos de utilização. A documentação foi escrita em Markdown, e convertida para HTML usando o projeto mkdocs5. Os exemplos de código disponibilizados na documentação são coloridos pelo módulo Pygments, usando a definição de linguagem que descrevemos na secção anterior. 5.1.5.3 Relatórios de Erros A aplicação de linha de comandos que interpreta a linguagem também dispõe de um componente responsável por, quando ocorre algum erro relacionado com a linguagem, imprimir para o terminal o sítio onde o erro ocorreu, bem como a mensagem a descrever o erro. Existem dois tipos de erros possíveis: erros de sintaxe, que ocorrem durante a fase de parse quando algum carácter está no sítio errado; e erros de execução, que ocorrem quando a linguagem está a executar e podem ter causas variadas. Para ambos os erros, o componente de relatório precisa apenas dos seguintes campos: Ficheiro Qual o caminho do ficheiro onde ocorreu o erro (opcional). 4https://pedromsilvapt.github.io/miei-dissertation/ 5https://www.mkdocs.org/ 49 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA Figura 5.2: Captura de ecrã da página de documentação da linguagem Figura 5.3: Relatório de erro de parse Código O conteúdo do ficheiro que originou o erro. Este campo pode parecer redundante, tendo nós o caminho do ficheiro. Seria de esperar que podíamos simplesmente ler o ficheiro e obter o seu conteúdo. No entanto, desde o momento que o ficheiro foi lido inicialmente, e o momento em que ocorreu o erro, o seu conteúdo pode ter mudado. Por esta razão, é importante manter em memória o código dos ficheiros carregados para assegurar que os erros são mostrados corretamente. Posição Início e Fim Dois inteiros indicando o inicio e fim da secção contígua no ficheiro onde o erro deve ser marcado a vermelho. Podem cobrir mais do que uma linha. Mensagem Finalmente, qual a mensagem de erro a mostrar ao utilizador. 50 5.1. ANÁLISE SINTÁTICA Erros de Sintaxe Estes erros são fáceis de reportar. Sempre que há algum erro de sintaxe, o parser emite uma exceção já com a informação da mensagem de erro, bem como a posição onde o erro ocorreu. Na nossa aplicação, só temos de tratar essa exceção, adicionando-lhe o caminho e o conteúdo do ficheiro a que estávamos a fazer parse, e já temos toda a informação disponível para mostrar o erro. Erros de Avaliação Este tipo de erros pode ocorrer em qualquer lado. Por essa razão, antes de fazer-mos parse de qualquer ficheiro, associamos o ficheiro (e o seu conteúdo) a um inteiro único por execução. Depois durante a fase de parsing, associamos a cada nó da AST um triplo com três inteiros, representando o ficheiro, posição de início, e posição de fim a que esse nó corresponde. Desta forma, no método Node.eval() herdado por todos os nós da AST, colocamos um try ... except ... à volta da chamada de Node.__eval__() (função que contém o código específico de execução de cada nó). Desta forma, conseguimos adicionar a qualquer exceção que ocorra durante a execução, o triplo que o nó tem com a informação sobre o ficheiro e a sua posição no mesmo. Esta informação é depois utilizada para preencher os campos e mostrar o relatório de erro. 51 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA 5.2 Semântica Dinâmica As linguagens compiladas usualmente recorrem à compilação Ahead Of Time (AOT), onde o código é transformado em código máquina antes de ser executado (durante a fase de compilação). Esta é a solução que consegue geralmente oferecer melhor performance, menor consumo de memória e tempos de startup mais rápido. Por outro lado, obriga a um passo de compilação separado sempre que o código fonte é alterado, e regra geral necessita de tipos de dados estáticos para melhor tirar partido das vantagens de performance. No que toca a linguagens interpretadas temos mais opções. Podemos então dividir os seus modos de execução em três categorias distintas, cada uma com possíveis vantagens e desvantagens, bem como diferenças na dificuldade de implementação que são bastante salientes. Interpretadores Também chamados por vezes como interpretadores tree walk, são usualmente os mais simples de implementar (mas também os mais lentos a executar). O seu conceito baseiase no design pattern homónimo, em que as operações da linguagem são modeladas numa árvore, geralmente igual ou similar à estrutura da AST. Para executar uma expressão é chamado um método na raiz da árvore, e esse método irá chamar recursivamente os métodos das suas sub-expressões, passando o estado como argumentos da função, e recebendo o resultado pelo valor retornado da função. Bytecode VM São chamadas máquinas virtuais (VM) porque o seu comportamento assemelha-se mais ao comportamento dos processadores reais dos computadores. As expressões da árvore de sintaxe abstrata são previamente convertidas numa sequência linear de instruções mais simples (geralmente compactadas em binário para melhor performance), também referidas como bytecode. Cada instrução é depois executada dentro de um ciclo. Esta solução fornece um balanço entre facilidade de implementação e velocidade de execução (evitando a grande quantidade de chamadas recursivas de funções presentes nos interpretadores tree walk). Just In Time O método mais complexo (mas também o que oferece melhor performance para tarefas pesadas). O código é executado inicialmente por um dos dois métodos anteriores, de modo a recolher estatísticas sobre qual o tipo de execução mais comum do código. Após esta recolha, é gerado código máquina otimizado para os tipos de dados mais comuns de uma variável, ou para os caminhos de execução mais prevalentes, para que seja possível da próxima vez que o mesmo pedaço de código seja executado, isso ocorra com recurso ao código máquina. Este processo é geralmente repetido ao longo da execução do programa, sendo que caso a versão de código máquina gerada fique desatualizado (o tipo de dados usualmente passados a uma função mudem), essa porção de código seja invalidada e eventualmente substituída por uma nova versão mais adequada. É possível ver que a solução ideal seria sempre a compilação Just In Time (JIT), que evita uma fase de compilação explícita e forçada ao utilizador, mas ao mesmo tempo consegue fornecer performance 52 5.2. SEMÂNTICA DINÂMICA competitiva para tarefas mais exigentes. No entanto é inevitável concluir que esta solução impõe custos de desenvolvimento astronómicos, e implica equipas de grande dimensão e tempos de desenvolvimento extremamente longos. Desta forma a nossa escolha reside logicamente entre a solução de Interpretador e uma simples Bytecode VM. Acabamos por escolher a primeira opção pelas seguintes razões: • A geração de eventos músicais é relativamente barata em termos computacionais (mesmo admitindo algumas dezenas de eventos músicais por segundo). • A componente mais pesada geralmente reside na sintetização dos eventos músicais (note on, note off) em streams de audio, mas esta tarefa é encaminhada para bibliotecas mais optimizadas e desenvolvidas em linguagens de baixo nível. • A possibilidade de correr código Python em qualquer parte da nossa linguagem já fornece um bom meio termo para quando é necessária mais alguma performance em caminhos de execução críticos (hot paths), sem sacrificar demasiado a simplicidade de uma linguagem destinada primariamente a músicos e não engenheiros informáticos. • Também a facilidade de implementação de um interpretador significou uma velocidade ordens de magnitude superior do que seria possível de outra forma, durante a implementação da linguagem e da prototipação de funcionalidades. • Uma posterior modificação do interpretador para uma bytecode VM mais eficiente seria possível estando a linguagem mais estabilizada, e seria simples manter uma API virtualmente 100% compatível. Em conclusão, cada operação foi implementada através de um método eval() em cada classe da AST, recebendo uma variável de contexto como input, e usando depois o valor retornado pela função como resultado da avaliação da expressão. 5.2.1 Contexto A variável de contexto guarda o estado da execução, e deve conter toda a informação necessária para cada instrução poder executar. Os seus conteúdos podem ser divididos em três componentes: Timestamp Esta propriedade é um simples inteiro que permite às expressões de geração de eventos musicais saberem qual o tempo virtual atual. Este tempo virtual é manipulado pelos vários operadores. O operador sequencial por exemplo, que recebe uma lista de expressões e emite uma lista de eventos musicais uns a seguir aos outros, avança este cursor para o fim do último evento antes de passar o contexto para avaliar a expressão seguinte. Voz Objeto que contém as várias propriedades musicais que devem ser aplicadas durante a geração de eventos, tais como a duração base das notas, o compasso ou as batidas por minuto. 53 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA Símbolos Um contentor que permite aceder e modificar os símbolos disponíveis na linguagem (tais como variáveis e funções). Os contextos foram desenhados com o intuito de serem leves, daí apenas conterem referências para três variáveis. Desta forma é possível criar cópias dos contextos e permitir que operações variadas estejam a executar ao mesmo tempo com diferentes contextos. Para percebermos a razão desta necessidade, basta pensarmos no operador paralelo. Se colocarmos duas expressões musicais que devem tocar ao mesmo tempo, a geração de uma (ou mais) notas na primeira expressão não deve afetar o timestamp da segunda. Para isto, cada uma das expressões deve receber uma cópia do contexto (com o timestamp inicial igual). Quando as duas expressões terminarem, no entanto, é importante que o contexto original (que gerou as duas cópias) atualize os seu timestamp para qual for a expressão mais longa. Se prestarmos atenção, podemos estar aqui a detetar um padrão bastante comum na área da programação: o modelo fork-join. Apesar de estarmos a lidar com notas e eventos musicais, fundamentalmente estamos a criar ramos paralelos de execução, e queremos no final aguardar o seu resultado e uníficá-lo com o ramo original. Se substituirmos ramo por thread para o caso da programação, ou contexto para o nosso caso, vemos a equivalência entre os conceitos. Como tal, para além do estado (as três variáveis) que o objeto de contexto engloba, este providencia também algumas operações bastante simples e úteis: Fork Cria uma cópia do contexto, podendo receber opcionalmente também um inteiro como argumento com vista a substituir o valor do timestamp do contexto pai. Como segundo argumento pode também receber um novo scope de símbolos, algo que iremos aprofundar mais no capítulo seguinte. Join Recebe uma lista de contextos como argumento, e avança o timestamp para o maior encontrado nessa lista. Seek A operação de mudar o timestamp do contexto atual. Os contextos não têm (nem precisam) de referências para outros contextos-pai ou contextos-filhos, de modo a que não é preciso grandes preocupações relativamente a fugas de memória com a criação de novos contextos: apenas são mantidos em memória os contextos que têm referências para eles, e que portanto estão ainda em uso. 5.2.2 Scope de Símbolos Cada contexto tem uma referência para o scope de símbolos a que tem acesso. Nestes scopes são guardadas as variáveis e funções que o utilizador (e as bibliotecas standard da linguagem) declararem. Mais uma vez, cada objeto de scope é composto por três propriedades: uma referência ao seu antecessor 54 5.2. SEMÂNTICA DINÂMICA (ou pai), uma tabela de hash com os símbolos, e uma flag booleana para designar este scope como opaco. O próprio conceito de scope implica por si só uma hierarquia, e de facto cada objeto scope guarda uma referência para o seu parente (ou para o valor nulo, caso seja o scope raiz). Esta propriedade é utilizada para as operações disponíveis, tanto para pesquisar, como para atribuir valores a símbolos do scope atual, algo que iremos verificar mais a seguir. Pesquisa A operação de pesquisa por um símbolo começa por procurar o símbolo no próprio scope, e caso não encontre nada, navega recursivamente para o scope pai para efetuar a pesquisa. Atribuição A operação de atribuição segue a mesma filosofia da operação de pesquisa, procurando o scope onde o símbolo a atribuir está guardado. No entanto, esta pesquisa decorre até encontrar o primeiro scope opaco. Um scope opaco não impede a leitura de valores de scopes superiores, mas impede a escrita. Por exemplo, uma atribuição dentro de uma função cria apenas uma variável dentro do scope dessa função. Se encontrar o símbolo nalgum scope, então muda o seu valor nesse scope onde está declarado. Caso contrário, é criado um novo símbolo no scope original onde a operação de atribuição foi iniciada. Enumeração A operação de enumeração permite listar quais os símbolos visíveis a partir de um scope. Ela permite listar não só os símbolos do próprio scope, mas também dos seus pais, tendo o cuidado para não retornar símbolos que tenham sido obscurecidos por um scope mais em baixo. Esta é útil para importar símbolos do scope de um módulo para outro, ou para implementar funcionalidades como autocomplete sobre um script em execução (e permitir sugerir os símbolos disponíveis). Apontadores Como vimos na atribuição, é impossível alterar valores de variáveis globais dentro de scopes opacos (como dentro de funções, por exemplo). Para colmatar isso, similar ao operador global enonlocal do Python, é possível instruir um scope a criar um apontador para um símbolo declarado noutro scope, de forma a que quando ocorre alguma atribuição, a alteração é replicada no seu scope original. 5.2.3 Módulos O Musikla dispõe também da possibilidade de executar código separado em vários ficheiros, cada um sendo considerado um módulo. O utilizador pode configurar uma lista de pastas (num conceito similar à variável de ambiente $PATH) onde serão procurados módulos quando alguma instrução import é avaliada. Também é possível passar um caminho absoluto ou relativo ao módulo que iniciou a importação. Cada ficheiro importado é executado apenas uma vez durante a primeira importação, e no final o seu scope é guardado em cache para ser usado em futuras importações. 55 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA Cada instrução de importação é dividida em duas fases: primeiro, o caminho passado é resolvido no caminho real e absoluto do ficheiro. Só depois é verificada na cache se o ficheiro já foi importado antes. Uma vez obtido o seu scope, os seus símbolos são enumerados e copiados para o scope que efetuou a importação (com símbolos que comecem com um underscore sendo tratados como símbolos privados e ignorados). Para isso, quando o interpretador é inicializado, é criado um scope raiz chamado prelude, e um scope filho. Cada módulo importado é executado num scope criado sempre a partir do prelude. Desta forma, os símbolos aí guardados estão sempre acessíveis em todos os módulos importados. 5.2.4 Operadores Musicais A sintaxe suporta os operadores aritméticos e de comparação usuais nas linguagens de programação. De modo geral, a sua sintaxe e semântica na nossa linguagem, o que seria expectável para cada um deles, pelo que não vale enumerá-los a todos nesta secção. No entanto, existem alguns operadores menos convencionais, ou até overloads de operadores convencionais (como o operador de multiplicação e adição) que funcionam de forma diferente quando empregues junto a valores do tipo musical. São esses os casos que iremos abordar nas seguintes sub-secções. Um aspeto importante a ter em conta é que estes operadores não estão restritos gramaticalmente para aceitar apenas expressões musicais, mas aceita sim qualquer tipo de expressão. O seu tipo (e portanto a sua semântica) são apenas decididos em runtime (imitando o modo de funcionamento do overloading de operadores em Python). Isto é necessário porque uma variável pode conter tanto um número como uma expressão musical, e o seu verdadeiro valor apenas é conhecido em execução. 5.2.4.1 Sequencial/Concatenação Ao longo deste documento já vimos várias instâncias de como declarar uma nota ou um acorde. O ato de concatenar esses eventos (ou seja, reproduzi-los em sequência) é tão fundamental para a linguagem que também é algo que já vimos inúmeras vezes nos exemplos sem pensar muito nisso. Uma das razões para isso deve-se à sua sintaxe (ou falta dela, na verdade). O ato de concatenar eventos musicais é tão simples como escrevê-los uns à frente dos outros, sem nenhum tipo de separador especial (para além dos opcionais espaços em branco). O evento ficam com o seu tempo inicial sempre igual ao tempo final do evento anterior. 1S4/4 T74 L/8 V90; 2A,EAB^cBAED^F^ce^cA3; Listagem 5.12: Excerto da música Wet Hands de C418 Também é importante salientar o facto de que as listas de instruções (statements) seguem a mesma semântica do operador de concatenação. Cada linha só vai tocar as suas notas quando as linhas anteriores 6Renderizado com a biblioteca $ABC_UI, com alguns ajustes manuais feitos para melhorar a clareza. 7Concatenação https://drive.google.com/open?id=1TP4lcul81s8iMCUFmD3HKnSpeftCzKT0 56 5.2. SEMÂNTICA DINÂMICA Figura 5.4: Pauta musical gerada pela linguagem6, versão áudio disponível aqui7. tiverem acabado. Neste exemplo portanto, ter tudo na mesma linha ou separado em duas (ou mais) tem o mesmo resultado, variando apenas a legibilidade. 5.2.4.2 Repetição O operador de repetição é bastante similar ao operador de concatenação. E faz sentido, uma vez que repetir uma expressão musical Nvezes pode ser pensado como um caso particular da concatenação onde existem Noperandos, todos iguais, uns em frente aos outros. 1I1 S6/8 T140 L/8 V90; 2A*11 G F*12 Listagem 5.13: Excerto do começo do tema principal de Westworld, por Ramin Djawadi Figura 5.5: Pauta musical gerada pela linguagem, versão áudio disponível aqui8. Este operador, apesar de bastante simples, é também bastante útil para repetir pequenos acompanhamentos (mesmo notas singulares) muitas vezes, ou até para repetir poucas vezes acompanhamentos mais complexos. Evitando repetir os acompanhamentos no código, é possível efetuar alterações em apenas um sítio e ver essas alterações repetirem-se ao longo de toda a estrutura musical. 5.2.4.3 Paralelo O operador paralelo permite reproduzir múltiplas sequências de eventos musicais em paralelo. Podemos dizer que conceptualmente, o operador paralelo é uma função que recebe uma lista de sequências musicais, e retorna uma única sequência com todas as sequencias combinadas. No entanto é importante que lembrar que o operador tem de respeitar os princípios de lazyness (não pedir mais eventos de cada vez do que o que os que são estritamente necessários) e ordem (a sequência de eventos resultantes tem de estar estritamente ordenada pelo tempo de início de cada evento). 8Repetição https://drive.google.com/open?id=1IIm8PQkLsNFMK9MNSVubJSG6SP6KwhPL 57 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA notas com o valor 15, pelo que adotamos esse valor como a nossa base (o que desperdiça 15 lugares em cada instrumento que poderiam ser associados a sons). Como na SoundFont podemos utilizar também vários instrumentos, isso significava que ainda assim conseguíamos reproduzir 127 ×113 =14351 sons distintos, o que é bastante mais do que suficiente para a maioria dos casos. 5.3.1.2 MIDI Uma funcionalidade extremamente importante e necessária para incluir de base foi o suporte para leitura e escrita de dados MIDI. Isto tanto pelo facto de que o standard MIDI é um dos mais utilizados no mundo da música, mas também porque a nossa implementação da linguagem, particularmente relativa à representação dos eventos musicais em memória, foi fortemente inspirada por este formato. Dessa forma, implementar funções de transformação dos não exigiu demasiado esforço. Para o efeito, utilizamos o módulo Python mido, que permite ler e escrever eventos MIDI, tanto de portas como de ficheiros em disco. A nível de leitura (input), o módulo disponibiliza uma função readmidi() que permite ler eventos musicais de um ficheiro ou porta MIDI, retornando um objeto do tipo Music. A função aceita os seguintes parâmetros (sendo os parâmetros de ficheiro ou de porta mutuamente exclusivos): file (Opcional) Quando presente, lê os eventos musicais de um ficheiro MIDI. port (Opcional) Permite ler os eventos de uma porta em vez de um ficheiro. Este parâmetro aceita vários valores. True Se receber este valor, tenta listar as portas MIDI disponíveis e escolher a mais indicada para ler os eventos. List[String] Uma lista de nomes de portas para escutar. Os eventos das portas referidas são combinados numa única sequência de eventos, como se viessem todos da mesma porta. String O nome de uma única porta para estar à escuta de eventos. voices (Opcional) Uma lista de vozes para mapear a cada canal MIDI a que os eventos pertencem. Quando nenhuma voz é especificada, a voz atual presente no contexto é utilizada para todos os canais. cutoff_sequence (Opcional) Uma sequência de notas ou eventos músicais que, quando recebidos pelas portas MIDI, são utilizados como sinal para terminar imediatamente a leitura e retornar a sequência de eventos já capturada. É ignorada quando a fonte de leitura é um ficheiro. ignore_message_types (Opcional) Uma lista de strings com os nomes de mensagens MIDI que devem ser ignoradas. 64 5.3. BIBLIOTECA STANDARD É também possível escrever (output) para portas e ficheiros MIDI de forma bastante simples. Este output é ativado automaticamente quando se grava um ficheiro com a extensão MIDI, ou quando se prefixa o nome de uma porta com a string midi://. Ao escrever para ficheiros ou portas MIDI, é possível filtrar apenas os eventos de certas vozes, e mapear também os canais atribuídos a cada voz. Isto é extremamente útil para permitir ligar a aplicação a programas DAW, e permitir receber os eventos MIDI em faixas separadas (para aplicar efeitos ou transformações específicas a cada faixa). 5.3.1.3 ABC Outro dos formatos de saída suportados é o formato abc notation. A sintaxe das notas e acordes da nossa linguagem é fortemente inspirada nesta notação, pelo que não seria estranho pensar que escrever para este formato não seria nada de especial. E em parte é verdade, na medida que gerar o texto com o formato certo não é complicado. A maior diferença é, no entanto, o facto de que o formato abc é estático, e a sua estrutura foi desenhada para imitar de certa forma a notação das partituras musicais. Tudo isto envolve conceitos como agrupar as notas por vozes, pautas, compassos, claves, entre outros. De certa maneira, o modelo de dados que a linguagem usa para representar as notas em memória mapeia-se de forma muito mais simples para o formato MIDI do que para o abc. Para colmatar este facto, os eventos, antes de serem convertidos para o formato abc, são introduzidos numa pipeline que executa uma série de passos de modo a tornar a conversão mais simples. De grosso modo, os passos pela ordem que são executados, são os seguintes: 1. Em primeiro lugar o sistema tenta converter quaisquer notas que possam estar a ser emitidas como eventos on/off separados. Isto acontece quando, por exemplo, o teclado é usado, o que pode implicar começar a tocar as notas sem saber quando o utilizador as vai parar. Os pares de eventos são então convertidos num só evento com a informação do início e da duração da nota. 2. Em segundo lugar, o sistema tenta identificar as notas paralelas que possam estar a ser emitidas e que possam pertencer a um acorde. Isto acontece mais uma vez quando, ao invés de usar a sintaxe de acordes presente na linguagem, o usa o teclado para gerar o acorde através de notas/teclas diferentes ativadas ao mesmo tempo. 3. Em terceiro lugar, as notas que pertençam à mesma voz mas que tenham overlap na sua duração, são colocadas em linhas diferentes. Mais uma vez, isto acontece mais frequentemente quando o teclado é usado, onde o utilizador pode estar a tocar notas concorrentemente da forma que quiser. 4. Em quarto lugar, são inferidos eventos pausa quando duas notas consecutivas pertencentes à mesma voz têm um intervalo. Mais uma vez, isto acontece mais frequentemente quando o utilizador toca com o teclado. Mas neste caso pode também acontecer quando as notas são declaradas pela 65 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA sintaxe da linguagem, como por exemplo na seguinte situação A(B|C), onde as notas AeB ficam na mesma pauta. A nota C, no entanto, como se sobrepõe com a nota B, pelo passo anterior, seria movida para a sua pauta. No entanto, esta pauta não teria nada ao início, e por isso precisa de uma pausa. Seria conceptualmente equivalente ao resultado de (A B) | (r C). 5. Finalmente em quinto lugar, são introduzidos eventos especiais na sequência musical para identificar, para cada voz em separado, quando devem ser introduzidos novos compassos, ou novas pautas na partitura. Do mesmo modo, isto implica que notas e acordes cuja duração não caiba num compasso, sejam divididos quantas vezes forem necessários, e conectados através de ligaduras. Estas transformações são reutilizáveis (não estão fortemente acopladas ao formato abc) e são implementadas utilizando o conceito de transformadores, que iremos abordar mais à frente neste documento. Muitas destas transformações são, no entanto, imperfeitas por natureza: a deteção de acordes a partir de notas paralelas individuais, por exemplo, nunca pode ter 100% de certeza se as notas que agrupou são realmente um acorde. É possível que o músico tivesse a intenção de que as várias notas pertencessem a vozes diferentes, por exemplo. Para além disto, a notação musical é tão complexa que apenas uma pequena parte é suportada pela nossa linguagem. Deste modo, é preciso ter em mente que apesar de a linguagem ser capaz de gerar ficheiros abc válidos, e com a sua estrutura maioritariamente correta de acordo com o que o utilizador tivesse em mente, serão muitas vezes necessários pequenos ajustes manuais para personalizar os resultados aos gostos de cada um. Ainda assim, o facto de não ser necessário escrever o ficheiro todo manualmente é por si só uma vantagem enorme. Isto porque o ficheiro abc não serve só como um formato de texto. Existem várias ferramentas para o converter para outros formatos, como PDF. Por exemplo, o projeto $ABC_UI14 é utilizado pela nossa linguagem para, em conjunto com o exportador abc, gerar páginas HTML com uma renderização da pauta usando Scalable Vector Graphics (SVG). Desta forma, podemos concluir que o formato abc serve como uma espécie de ponte standard, e nos permite apanhar boleia para depois suportar inúmeros outros formatos que existam muito mais facilmente. 5.3.2 Ficheiros de Som Como já foi mencionado no sub-capítulo sobre o output FluidSynth, uma das funcionalidades que queríamos implementar na linguagem era a possibilidade de reproduzir ficheiros de som arbitrários, e integra-los com o resto da geração de eventos musicais. Isto abre a possibilidade de incluir nos projetos que usem a linguagem sons gerados por outros programas, e mais uma vez vai de encontro ao nosso objetivo central de extensibilidade da linguagem. Podemos pensar assim que mesmo que a aplicação não suporte todo o tipo de geração de sons que alguma pessoa 14http://dev.music.free.fr/web-demo/\protect\TU\textdollarABC_UI.html 66 5.3. BIBLIOTECA STANDARD possa precisar, é sempre possível usar a aplicação para a maioria dos casos mais comuns que são suportados, e gerar ficheiros com recurso a alguma aplicação externa que colmatem as nossas necessidades mais específicas. Para utilizar-mos esta funcionalidade, temos à nossa disposição o método sample(). 1A B sample( ”cihat.wav” ); Listagem 5.18: Exemplo de reproduzir um ficheiro a seguir a duas notas Para facilitar todo o processo, os ficheiros de som devem seguir todos as mesmas especificações. Por conveniência, adotamos as configurações suportadas pela biblioteca FluidSynth para blocos de som nas suas SoundFonts. Configuração Valor Formato WAVE Sample Rate 41.100hz Canais 2 Bit depth 16bit Compressão N/A Tabela 5.3: Formato nativo suportado pelo FluidSynth Para facilitar a utilização, é possível ao utilizador carregar ficheiros de som em formatos diferentes desde que tenha a ferramenta FFmpeg instalada no sistema. Quando isso ocorre, a linguagem converte o ficheiro musical background, aplicando as configurações necessárias, e guarda-o depois em memória. Isto é especialmente útil para experiências rápidas e com ficheiros pequenos (até alguns megabytes). Esta funcionalidade traz bastante simplicidade à utilização da linguagem, mas impõe um custo durante a inicialização de todos os programas. Para permitir aos utilizadores determinarem se os seus ficheiros vão necessitar de conversão, sem terem de recorrer a ferramentas externas para efetuar a verificação, disponibilizamos a função is_sample_optimized(), em conjunto com a função optimize_sample() para converter o ficheiro e gravar o resultado em disco. 1if ( not is_sample_optimized( ”cihat.wav” ) ) { 2# Converts the file and saves it to disc 3optimize_sample( ”cihat.wav”, ”cihat_opt.wav” ); 4}; Listagem 5.19: Verificar se um ficheiro de audio está optimizado, e convertê-lo caso contrário Desta forma a conversão ocorre apenas uma vez, e de seguida o utilizador pode utilizar o ficheiro convertido e não se preocupar com a perda de performance sempre que usa o som num script. 67 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA 5.3.3 Grelhas A linguagem tem dois modos de produção de música principais: expressões musicais descritas no código, e a utilização de teclados em tempo real. Em termos de propriedades temporais, de modo geral, as expressões musicais são geradas com tempos precisos. Por outro lado, como é natural, músicas criadas através dos teclados musicais vão ter tempos imperfeitos. Quando utilizadas de modo separado, as diferenças muitas vezes são tão subtis que não se notam. Mas quando queremos tocar uma expressão de música pré-definida em código ao mesmo tempo que se utiliza um teclado, as diferenças e imperfeições podem-se tornar mais óbvias (Frühauf, Kopiez & Platz, 2013). A operação conhecida como Quantization 15 é possível na nossa linguagem através da utilização de Grelhas. Uma vez que o objetivo das grelhas é poderem ser utilizadas em tempo real à medida que as notas vão sendo tocadas, a forma de funcionamento escolhido para as grelhas é bastante simples e determinística, dependendo apenas do tempo (timestamp) e tipo do evento que queremos alinhar. Uma grelha é composta por um número infinito de células, todas do mesmo tamanho, que se estendem ao longo do eixo temporal. Quando um evento musical é tocado, esse evento vai calhar algures dentro de uma dessas células. A responsabilidade da grelha passa por determinar se o evento deve ser movido no tempo, bem como para onde deve ser movido. Para isso, devemos compreender como cada célula da grelha é dividida. Figura 5.11: Representação de duas células de uma grelha Na figura 5.11 podemos ver representadas duas células de uma grelha, cada uma com tamanho de 1 segundo. Também podemos ver que cada célula está dividida em duas partes, direita e esquerda, e a ordem dessas partes pode parecer engano, ou que estão trocadas. Mas estão corretas, pois na verdade cada uma dessas partes é relativa não à célula, mas ao separador das células. Olhemos por exemplo para o divisor central, situado em cima da marca do 1 segundo. A parte à sua esquerda refere-se à esquerda do separador, e a parte à direita referem-se à direita do separador. Desta forma, faz mais sentido a ordem das partes. Por predefinição, ambas as partes têm o tamanho igual a metade do tamanho da célula (ou seja, neste caso, tanto a parte da esquerda e da direita têm o tamanho de 0.5 segundos). No entanto, é possível customizar o tamanho das duas de forma igual, ou cada uma em particular, através das propriedades forgiveness erange. 1$grid = Grid( 1, 15https://en.wikipedia.org/wiki/Quantization_(music) 68 5.3. BIBLIOTECA STANDARD Figura 5.12: As várias áreas configuráveis de uma grelha 2forgiveness = 125, # 1/8 of a second 3range = 375 # 3/8 of a second 4); Listagem 5.20: Código de definição da grelha representada na figura 5.12 Não nos esqueçamos que o objetivo da grelha é alinhar eventos, movendo os eventos ao longo do eixo do tempo para junto dos separadores das células. A propriedade range tem como valor predefinido metade do tamanho da célula. Isso significa que tanto o range_left como o range_right cobrem de modos igual toda a célula, e dessa os eventos são movidos para o separador da grelha que estiver mais próximo. O valor predefinido da propriedade forgiveness, por outro lado, é zero, o que significa que mesmo os eventos que se encontrem já muito perto dos separadores são movidos. Na listagem 5.20 podemos ver a definição de uma grelha simétrica (as propriedades left eright são iguais). O valor de forgiveness implica que eventos que estejam a mais de 372 milissegundos de distância de qualquer um dos separadores são ignorados pela grelha, isto é, o seu tempo não se mexe. O valor de forgiveness, o que significa que eventos que estejam a menos de 125 milissegundos de um dos separadores são também ignorados pela grelha. Neste exemplo, são apenas movidos os eventos que estejam entre 125 e 375 milissegundos de distância de um dos separadores. Note-se que cada propriedade poderia ser costumizada para a esquerda e para a direita. Isto é, o exemplo acima podia ser reescrito da seguinte forma: 1$grid = Grid( 1, 2forgiveness_left = 125, # 1/8 of a second 3forgiveness_right = 125, # 1/8 of a second 4range_left = 375, # 3/8 of a second 5range_right = 375 # 3/8 of a second 6); Listagem 5.21: Código alternativo de definição da grelha representada na figura 5.12, com as propriedades left eright Se passarmos das propriedades de configuração para a grelha resultante, na verdade cada célula passa a ter dois effective ranges, duas áreas (uma para a esquerda e outra para a direita) onde os 69 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA eventos que aí calharem são movidos. Os eventos fora dessas áreas são ignorados. Figura 5.13: Áreas em que a grelha irá mover os eventos, e qual o separador a que pertencem. Tomemos como exemplo a seguinte situação com três eventos e a grelha definida anteriormente: o evento A, como calha dentro da área azul (devido à propriedade range_right), vai ser relocado para o tempo zero. O evento B no entanto, como não calha em nenhuma área (devido devido a propriedade forgiveness_left. Finalmente, o evento C como calha na área vermelha (devido à propriedade range_left é relocado para o tempo 2. Figura 5.14: Posição dos eventos após o alinhamento com a grelha ter sido aplicado. O modo de funcionamento das grelhas pode parecer à primeira vista um desnecessariamente complexo, ou os exemplos um pouco artificiais, mas o objetivo foi sempre permitir ao utilizador aplicar o tipo de grelha que mais se adequar ao seu caso. Isso pode ser uma simples grelha onde só define o tamanho de cada célula, e os ranges são cada metade de cada célula, empurrando todos os eventos para o separador que lhe esteja mais próximo, ou exemplos de grelhas assimétricas, onde possivelmente os eventos são sempre movidos só numa direção (esquerda ou direita) ou onde eventos que estejam perto dos separadores não sejam movidos. 5.3.4 Teclados Musicais Para além de permitir construir expressões que geram sequências de eventos musicais dinâmicas, algo que esteve desde início no topo da nossa lista de prioridades foi sem dúvida adicionar suporte para a criação de teclados interativos. Sendo esta linguagem desenvolvida em computadores, não seria errado pensar que por teclado nos referimos aos teclados físicos dos computadores. E esses fazem certamente parte, mas quando nos referimos a teclados musicais, referi-mo-nos à possibilidade de descrever na nossa linguagem mapeamentos entre eventos e expressões musicais ou ações a executar quando esses eventos acontecem. 70 5.3. BIBLIOTECA STANDARD 1@keyboard { 2a: print( ”Tecla a carregada” ); 3b: d; 4}; Listagem 5.22: Exemplo de declaração de duas teclas Esses eventos podem então ser teclas de um teclado, mas também de um piano conectado ao computador, ou eventos do rato, ou de qualquer outro dispositivo de I/O que as pessoas queiram adaptar, bastando para isso herdar a classe KeyboardEvent. 5.3.4.1 Tipos de Eventos Os tipos de evento mais comum são teclas de teclado e de piano. Por essa mesma razão implementamos açúcar sintático na definição desses eventos (que são implementados pelas classes KeyStroke e PianoKey, respetivamente). 1@keyboard { 2ctrl+c: ^c; 3[16]: d; 4[c']: e; 5}; Listagem 5.23: Declaração de três eventos, o primeiro é uma combinação de teclas, o segundo referência ovirtual key code, e o terceiro uma nota MIDI A lista de tipos de eventos suportados de base pela linguagem são os seguintes: KeyStroke Este tipo de eventos referem-se às teclas do teclado. Para além de permitirem descrever teclas singulares, também suportam os modificadores ctrl,alt eshift. O evento não carrega consigo nenhum parâmetro extra. Parâmetros: N/A PianoKey Sinalizam quando uma nota é tocada e recebida pela porta MIDI que a aplicação esteja à escuta. Trazem consigo um parâmetro que identifica a velocidade com que a nota foi premida. Parâmetros: $vel MouseClick Evento ocorre quando algum dos botões do rato é premido. As informações sobre a posição em que o rato se encontra, bem como qual o botão premido e se foi premido ou levantado, são incluídas como parâmetros deste evento. Parâmetros: $x,$y,$button $pressed MouseMove Evento ocorre sempre que o rato é movido. Pode ser útil para controlar alguma variável como se fosse um slider. Trás como parâmetros as coordenadas do rato. 71 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA Parâmetros: $x,$y MouseScroll Evento despoletado quando a roda do rato é acionada. Para além de trazer informações sobre a posição do rato, indica também qual o valor que a roda se moveu, tanto na vertical (mais comum) como também na horizontal. Parâmetros: $x,$y,$dx,$dy Os parâmetros são algo que, como pudemos ver, é disponibilizado por quase todos os tipos de eventos. Eles dão-nos a possibilidade de saber que os eventos foram despoletados, mas saber propriedades sobre o que os despoletou. Estes parâmetros podem ser acedidos passando as variáveis desejadas com o nome do parâmetro (a ordem é irrelevante) à frente da declaração do evento. Essas variáveis podem depois ser acedidas dentro da ação do evento. 1@keyboard { 2[keyboard\MouseMove] ($x, $y): print( $x, $y ); 3}; Listagem 5.24: Teclado que imprime as coordenadas do rato sempre que ele se move Para criar novos tipos de eventos, basta criar uma nova classe em Python que derive da classe KeyboardEvent. Esta classe deve implementar apenas dois métodos obrigatórios (__hash__ e__eq__) que são usados para guardar os eventos num dicionário, ficando cada evento associado à sua ação. Opcionalmente pode ser definido um terceiro método, get_parameters, que deve retornar um dicionário com as variáveis e os seus respetivos valores que o evento disponibiliza. Cada tipo de evento pode também definir uma propriedade chamada binary como verdadeira ou falsa. Os binários referem-se ao conceito de eventos que são conceptualmente compostos por duas fases: premir e soltar. Exemplos deste tipo de eventos são, obviamente premir e soltar teclas do computador ou de um piano. A razão porque este conceito é tratado como um caso particular na nossa linguagem, ao invés de serem tratados como dois eventos separados (PianoKeyPress ePianoKeyRelease por exemplo), deve-se ao facto de os eventos binários mapearem de forma bastante elegante com o conceito de note on enote off na geração de música. E da mesma maneira que a nossa linguagem não obriga o utilizador a declarar por cada nota o seu ponto de início e de fim separados, não faria sentido fazer isso para os teclados. Por essa razão, os teclados podem (através dos modificadores hold extend que vamos cobrir a seguir) mapear automaticamente o início e o fim das notas declaradas em cada uma das suas teclas, com os modos press erelease dos seus eventos. 5.3.4.2 Modificadores de Eventos Cada evento de um teclado tem associada uma ação: essa ação pode ter size effects, e opcionalmente retornar um evento musical (ou uma sequência de eventos). Sempre que o evento é despoletado, a ação é avaliada e caso retorne um objeto musical, esse é reproduzido. 72 5.3. BIBLIOTECA STANDARD Por predefinição, o tempo que a tecla está premida não afeta a duração das notas emitidas. Pelo contrário, a duração das notas (e acordes e todo o resto de eventos musicais) é calculada com o que o utilizador tiver determinado no código. Isto acontece porque o teclado permite que o utilizador defina não só um evento musical para cada tecla, mas sim que possa definir uma música completa, se quiser. Neste caso quando a tecla é premida, a música começa a tocar até terminar. Os modificadores permitem customizar o comportamento do teclado quando encontra alguma sequência musical. Cada modificador pode ser aplicado a todo o teclado (quando aparece à frente da keyword @keyboard), ou ser aplicada individualmente a cada tecla. repeat Quando presente, o teclado reproduz a música da tecla, e quando esta acabar, começa a tocar novamente, sem parar. toggle Reproduz a música da tecla até esta ser premida novamente (ou a música acabar). hold Reproduz a música da tecla enquanto a tecla estiver premida (ou a música acabar). extend Tanto o toggle como o hold permitem terminar uma música mais cedo. Com este modificador, todas as notas tocadas pela tecla ficam ativas enquanto a pessoa não carregar novamente na tecla (em conjunto com o toggle), ou a largar (em conjunto com o hold). Assim, se quisermos replicar o comportamento de um piano, por exemplo, em que premir a tecla começa a tocar uma nota, e liberta-la para a nota, podemos usar os modificadores hold extend em conjunto. 1@keyboard hold extend { 2a: c; 3s: d: 4d: e; 5}; Listagem 5.25: Aplicar o modificador hold extend a um teclado inteiro 5.3.4.3 Estruturas de Controlo Até agora temos visto como é possível declarar manualmente ações num teclado. Algo que ainda não foi dito é o facto de o bloco de declaração de um teclado @keyboard {} ser uma macro que, antes da execução, é traduzida para instruções que criam o teclado e registam as teclas. 1{ 2$__keyboard = keyboard\create(); 3keyboard\push_flags($__keyboard; 'hold'; 'extend'); 4keyboard\register($__keyboard; 'a'; c); 5keyboard\register($__keyboard; 's'; d); 6keyboard\register($__keyboard; 'd'; e); 73 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA No futuro, era interessante melhorar o auto-complete para completar sub-propriedades de objetos e listas, bem como mostrar informações adicionais sobre as sugestões, tal como os seus tipos e no caso de serem métodos, quais os parâmetros que aceita. 5.3.6 Transformadores Os transformadores são uma solução para um problema prático que surgiu durante o desenvolvimento do projeto, ao invés de serem uma funcionalidade pública da linguagem. No entanto a razão pela qual foram necessários, bem como a solução desenvolvida, podem ser interessantes de serem discutidas. Para compreendermos melhor a razão da sua necessidade, imaginemos o seguinte caso. 5.3.6.1 O Problema Um dos tipos de dados mais importantes na nossa linguagem é o tipo música, que como já referimos várias vezes pode ser representado como uma sequência de eventos musicais. Quando pensamos em sequências, é fácil imaginar uma variedade de operações genéricas possíveis de serem aplicadas nelas, como map efilter. Mas também existem operações mais específicas ao nosso caso que podemos querer aplicar, como dada uma sequência de eventos, separar os NoteEvent em dois NoteOnEvent eNoteOffEvent. Isto à primeira vista pode parecer um simples flatMap, em que alguns eventos são transformados em dois a serem inseridos na sequência. No entanto, uma vez que os eventos nas nossas sequências devem viajar sempre ordenados, não podemos simplesmente inserir o evento off junto do on, porque podem existir outros eventos que comecem ao mesmo tempo ou depois da nossa nota começar, mas antes dela acabar. Nesse caso basta construirmos uma algoritmo simples que guarde num buffer ordenado os eventos off, e os vá inserido imediatamente atrás do primeiro evento com um timestamp maior que os seus. E como este, existem vários algoritmos que podem ser usados e reutilizados em diversas partes da aplicação com os mais diversos propósitos de transformar sequências de eventos. O problema surge quando a fonte da música pode ser sincrona (no caso de uma expressão da nossa linguagem) ou assíncrona (no caso de um teclado ou piano, em que os eventos vão sendo gerados em tempo real). Python suporta assincronia através do módulo asyncio que pode ser usado em conjunto com a sintaxe async/await. Para simplificar os exemplos seguintes, tomemos o caso da operação genérica map. As suas implementações, nas variantes síncrona e assíncrona, poderiam ser descritas como visto na listagem 5.37. 1def map (fn): 2for event in music: 3yield fn(event) 4 5async def map_async (fn): 6async for event in music: 7yield fn(event) 80 5.3. BIBLIOTECA STANDARD Listagem 5.36: Exemplo de uma função map com versões sincronas e asíncronas Como podemos ver, quando a única diferença no algoritmo é se a fonte é síncrona ou assíncrona, a variação é mínima. Mas a diferença é sintática, por isso teríamos duas escolhas: • Para cada algoritmo criar duas versões, uma síncrona e outra assíncrona. Mas isto levava a duplicação de código particularmente má, porque o que iria mudar eram apenas duas palavras em todas as funções. • Usar sempre a versão assíncrona, mesmo quando a fonte dos dados é síncrona. Isto porque é sempre possível converter uma sequência de dados síncrona em assíncrona, mas o inverso já não é. O problema desta solução é o facto de as funções assíncronas terem cor16, e só poderem ser chamadas por funções da mesma cor. Qualquer função que potencialmente lidasse com expressões musicais teria de passar a ser assíncrona. E como a nossa linguagem tem tipos dinâmicos, potencialmente qualquer expressão pode retornar uma sequência musical, logo todas as expressões na nossa linguagem teriam de ser executadas em modo assíncrono, o que traria uma penalização a nível de performance difícil de justificar. 5.3.6.2 A Solução Escolhida A solução passou por criar uma abstração a que chamamos Transformador, que contém um bocado das duas opções anteriores: Os algoritmos são declarados apenas uma vez, e são depois envoltos num transformador que disponibiliza duas interfaces públicas, uma síncrona e outra assíncrona. Cada transformador tem uma interface pública composta por 4 funções que permitem simular um iterador: add_input( ev ),end_input(),add_output( ev ) eend_output(). Para evitar chamar manualmente as funções e tratar do controlo do fluxo de execução manualmente, são incluídos dois métodos de classe em todos os transformadores que permitem correr um iterador (síncrono ou assíncrono). As funções, chamadas iter eaiter recebem (e retornam) respetivamente um iterador síncrono e assíncrono. Vejamos agora um exemplo do transformador map na listagem 5.37. Pode parecer bastante mais verbosa do que as funções map iniciais, mas é importante notar que este custo é fixo: só esta parte do algoritmo muda, e todo o resto seria igual. Como os transformadores utilizados no projeto são bem mais complexos do que este pequeno exemplo (podendo ter mais de uma centena de linhas de código), este custo fixo de escrever cerca de seis linhas de código boilerplate por transformador não é um problema tão grande. 1class MapTransformer(Transformer): 2def __init__ (self, fn): 3self.fn = fn 16https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ 81 CAPÍTULO 5. DESENVOLVIMENTO DO INTERPRETADOR MUSIKLA 4 5def transform ( self ): 6while True: 7done, value = yield 8 9if done: break 10 11 self.add_output( self.fn( value ) ) Listagem 5.37: Implementação da função map usando a nossa abordagem de transformadores Todos os transformadores implementam uma função gerador transform, que é a peça central de tudo isto. Aí dentro, temos um ciclo responsável por emular a iteração sobre a sequência de input. Depois utilizamos a instrução yield para obter o próximo valor. Aqui estamos a aproveitar o facto de esta instrução não permitir apenas enviar valores para fora do gerador (que é o seu uso mais comum), mas também receber valores. Desta forma o gerador fica em pausa até receber o próximo evento. A vantagem desta pausa é que pode ser retomada logo de seguida se estivermos a trabalhar com iteradores síncronos, ou pode ficar assim até o próximo valor ficar disponível, no caso de um iterador assíncrono. 1# Irá imprimir 0, 2, 4, 6, 8 2for ev in MapTransformer.iter(range( 5 ), lambda e:e*2): 3print( ev ) Listagem 5.38: Exemplo de um transformador map e da sua utilização Na listagem 5.38 podemos ver a utilização do transformador aplicada a uma lista (já que também implementam a interface de um iterador). O método aiter poderia ser usado de forma análoga com um async for e com um iterador assíncrono. 5.4 Resumo do Desenvolvimento O processo de desenvolvimento foi bastante ágil, sendo estruturado de uma forma iterativa, em que a cada semana eram implementadas novas funcionalidades. Estas podiam depois ser postas à prova, aplicadas a casos de estudo, e possivelmente melhoradas numa iteração futura. O tempo dedicado a cada um dos componentes da linguagem Musikla foi intercalado entre a análise sintática, semântica dinâmica e a biblioteca standard. Nem todas as funcionalidades exigiram esforço ou tempo equivalentes, e apesar de linhas de código não mapearem perfeitamente para tempo despendido, permitem ter alguma noção aproximada do mesmo. Obviamente sendo estes números apenas uma contagem das linhas atuais, não são uma representação total de tudo o que foi programado, pois não têm em conta partes do código que foram reescritas ao longo do desenvolvimento, como foi o caso da gramática e do reconhecedor sintático, por exemplo. 82 5.4. RESUMO DO DESENVOLVIMENTO Componente Linhas de Código Percentagem Análise Sintática 1090 11% Semântica Dinâmica 5268 53% Biblioteca Standard 3527 36% Total 9885 100% Tabela 5.4: Linhas de código (sem linhas vazias ou de comentário) do projeto Musikla Para além destes componentes, foi desenvolvido para ser usado na documentação um utilitário em JavaScript17 com cerca de 500 linhas de código, que permite descrever e gerar os diagramas sobre grelhas (também usados nesta dissertação). A documentação está escrita no formato Markdown e conta também com um pouco mais de 700 linhas de texto. Este número não pode no entanto ser comparado diretamente com as linhas de código, já que texto em prosa e código são conceitos diferentes. Ainda assim ajudam a ter ideia do que foi feito, para além da escrita da desta dissertação, do artigo publicado e do desenvolvimento do interpretador Musikla. 17Código disponível em https://github.com/pedromsilvapt/miei-dissertation/blob/master/code/musikla/docs/assets/ generator.js 83 Capítulo 6 Guia Rápido de Utilização O resultado final do processo de desenho e desenvolvimento descrito acima foi um módulo Python publicado no repositório PyPi chamado musikla. Este módulo pode ser usado através da linha de comandos, ou os seus sub-módulos podem ser usados diretamente num projeto Python. Nesta secção vamos fazer uma pequena overview dos diversos modos de como o módulo pode ser utilizado (versão da documentação mais completa disponível em na web). A instalação é bastante simples, sendo suficiente correr o comando seguinte comando: 1pip install musikla Listagem 6.1: Processo de instalação do package musikla É importante notar que uma dependência central da linguagem é a biblioteca nativa FluidSynth. Apesar de existir uma versão 2.x, essa não é suportada atualmente, pelo que é necessário o utilizador certificar-se que o seu sistema tem a versão 1.x instalada. 6.1 Módulos O projeto musikla está dividido em bastantes sub-módulos, os mais importantes dos quais são listados aqui: musikla.core.theory Contém classes e funções auxiliares relativas à teoria músical, utilizadas por diversos outros sub-módulos. musikla.core.events.transformers Alberga os transformadores responsáveis por separar notas musicais em eventos On/Off, por exemplo, ou por tentar agrupar notas concorrentes em acordes, se for o caso disso, entre muitos mais. 85 CAPÍTULO 6. GUIA RÁPIDO DE UTILIZAÇÃO musikla.core.events Contém as classes que representam os eventos musicais em memória, desde os mais óbvios, como o NoteEvent,ChordEvent ou RestEvent, mas também outros menos óbvios como o ControlChangeEvent, por exemplo. musikla.core Disponibiliza muitas das classes mais fundamentais para o funcionamento da linguagem, tais como Context,Music,Instrument,Voice,SymbolsScope, entre outros. musikla.parser.abstract_syntax_tree Contém as classes que representam os diversos nós da AST. musikla.parser Disponibiliza as classes Parser, responsáveis por criar uma AST com base numa string ou num ficheiro contendo código Musikla, bem como a classe CodePrinter responsável pelo processo oposto, transformando de volta uma AST numa string usando a sintaxe da linguagem. musikla.audio.sequencers Alberga os diversos sequenciadores (formatos de saída) suportados nativamente pela linguagem. musikla.audio Contém as classes Player eInteractivePlayer que permitem reproduzir sequências musicais para os sequenciadores. musikla.libraries Disponibilizam as várias classes que definem as funções e símbolos expostos para a linguagem Musikla pela biblioteca standard. musikla O módulo principal, contém as duas classes responsáveis por inicializar todos os sistemas necessários para a linguagem funcionar, CliApplication eScript. 6.2 Configuração Quando a aplicação é inicializada, é opcionalmente carregado um ficheiro musikla.ini se este se encontrar na pasta Home do computador do utilizador. Este ficheiro permite configurar diversos dos valores por predefinição utilizados pela linguagem. A maioria destas propriedades pode ser substituída e costumizada em cada execução quando a linguagem é executada pelo terminal. 1[Musikla] 2soundfont = /path/to/soundfont.sf2 3output = -o alsa 4midi_input = midi input port name 5path = colon : separated list of paths to search for when importing files 6prelude = colon : separated list of paths to include in the prelude (what is available to all ,→modules) 7autoload = colon : separated list of paths to execute before the main file does 8 9[FluidSynth.Settings] 10 audio.periods = 2 86 6.3. LINHA DE COMANDOS 11 audio.period-size = 64 12 synth.sample-rate = 44100 Listagem 6.2: Exemplo de um ficheiro de configuração da linguagem O valor da propriedade soundfont indica o caminho do ficheiro .sf2 a usar pela linguagem para sintetizar os sons. A propriedade output permite indicar qual ou quais os sequênciadores (e os seus parâmetros) a serem por predefinição utilizados quando o utilizador não especifica nenhum em particular. A sua sintaxe é explicada em mais detalhe no capítulo seguinte. A propriedade midi_input permite indicar o nome da porta MIDI a usar quando algum teclado está à escuta de eventos MIDI. A propriedade path permite listar um conjunto de caminhos que serão procurados sempre que o utilizador importe algum ficheiro global nos seus scripts. A propriedade prelude indica uma lista de scripts Musikla a serem executados e carregados, e cujos símbolos exportados devem ficar disponíveis para todos os outros módulos. A propriedade autoload é similar à prelude, mas os símbolos apenas são disponibilizados para o ficheiro que o utilizador estiver a correr diretamente (o ficheiro entry point, podemos dizer). Na secção FluidSynth.Settings podemos alterar manualmente as propriedades passadas ao sequênciador FluidSynth. A lista de propriedades e valores suportados está disponível aqui1. 6.3 Linha de Comandos A linha de comandos pode ser executada através do comando musikla. Podemos executar um ficheiro passando o seu caminho como argumento. Também podemos correr musikla –help para vermos uma listagem dos argumentos suportados. 1musikla file.mkl --soundfont custom_soundfont.sf2 2-o alsa --gain 1 3-o music.wav 4-o musikla_port -f midi --port Listagem 6.3: Exemplo de um ficheiro de configuração da linguagem O comando pode ter uma lista de seuquênciadores (ou outputs) para utilizar. Estes devem vir sempre no fim do comando, depois de todos os argumentos e opções globais, e são identificados pela opção -o ou –output. O tipo do sequênciador é geralmente inferido pelo valor passado ao output, mas caso haja ambiguidade, é possível identificar manualmente o formato com a opção -f ou –format. Também são passadas a esse output todas as opções que estiverem depois do -o e até ao fim do comando, ou até ao próximo -o encontrado. As opções aceites variam com o formato do output. 1FluidSettings.xml http://www.fluidsynth.org/api/fluidsettings.xml 87 Capítulo 7 Conclusão O desenvolvimento do projeto envolve vários aspetos, desde o desenho da sintaxe e da gramática correspondente, até à geração de sons a serem guardados em ficheiros ou reproduzidos imediatamente em dispositivos áudio. Também engloba aspetos comuns em todas as linguagens de programação, em conjunto com questões mais específicas sobre como gerir o conceito de tempo na linguagem, de laziness para gerar apenas as notas necessárias, entre muitos outros. Para além disso também abrange quais as ferramentas e as metodologias que são mais adequadas para a utilização da linguagem. É necessário para isso estudar com exemplos reais, qual a melhor forma de produzir e desenvolver música em formato textual. Quais as funções e as suas interfaces que são mais úteis disponibilizar logo à partida a todos os utilizadores. No entanto, sendo a criação musical uma área tão ampla, é inútil tentar sequer conseguir desenvolver uma ferramenta que cubra todos os cantos e sirva todas as necessidades dos seus potenciais utilizadores. É por isso que é importante apoiar o desenvolvimento do projeto nas vantagens que a linguagem Python fornece, quer a nível da sua facilidade de uso, popularidade, e fácil extensão sem necessidade de complicados processos de compilação. O projeto foi por isso desenvolvido com extensibilidade em mente, tendo como objetivo principal servir como uma fundação estável capaz de ligar as diversas ferramentas existentes, não só na área da música, mas permitir ligar a música a outras áreas. Durante o desenvolvimento, tornou-se claro que criar música através de programação é uma forma extremamente poderosa de se trabalhar em muitas situações, mas não é uma solução perfeita para todos os casos. Com isto em mente, sentimos que a nossa escolha de incluir diretamente na nossa linguagem a possibilidade de programar teclados permite cobrir os momentos em que o utilizador precisa de uma forma mais interativa e imediata de tocar e compor músicas. Para além disso, as funcionalidades extra de permitir gravar e reproduzir as performances em buffers enquanto os teclados estão ativos, bem como a inclusão de um simples editor de código que permite escrever e executar código em runtime 89 APÊNDICE A. GRAMÁTICA PEG 28 arguments <- single_argument ( _ arguments_separator _ single_argument )*; 29 30 single_argument <- single_argument_prefix _ ”$” identifier _ ”=” _ expression 31 / single_argument_prefix _ ”$” identifier 32 ; 33 34 single_argument_prefix <- ”expr” / ”ref” / ”in” / ””; 35 36 arguments_separator <- ',' / ';'; 37 38 for_variables <- variable ( _ arguments_separator _ variable )*; 39 40 for_loop_head <- ”for” _ ”(” _ for_variables _ ”in” _ value_expression _ ”..” _ ,→value_expression _ ”)” 41 / ”for” _ ”(” _ for_variables _ ”in” _ value_expression _ ”)” 42 ; 43 44 for_loop_statement <- for_loop_head _ ”{” _ body? _ ”}”; 45 46 while_loop_statement <- ”while” _ ”(” _ expression _ ”)” _ ”{” _ body _ ”}”; 47 48 if_statement <- ”if” _ ”(” _ expression _ ”)” _ ”{” _ body _ ”}” _ ”else” _ ”{” _ body _ ”}” 49 / ”if” _ ”(” _ expression _ ”)” _ ”{” _ body _ ”}” 50 ; 51 52 return_statement <- ”return” _ expression?; 53 54 // BEGIN Keyboard 55 keyboard_declaration <- ”@keyboard” (_ alphanumeric)* ( _ group )? _ ”{” _ keyboard_body _ ”}” ,→; 56 57 keyboard_body <- keyboard_body_statement ( _ ”;” _ keyboard_body_statement )* _ ”;”? 58 / ”” 59 ; 60 61 keyboard_body_statement <- keyboard_for 62 / keyboard_while 63 / keyboard_if 64 / keyboard_block 65 / keyboard_shortcut 66 ; 67 68 keyboard_for <- for_loop_head _ ”{” _ keyboard_body _ ”}”; 69 70 keyboard_while <- ”while” _ ”(” _ expression _ ”)” _ ”{” _ keyboard_body _ ”}”; 96 71 72 keyboard_if <- ”if” _ ”(” _ expression _ ”)” _ ”{” _ keyboard_body _ ”}” _ ”else” _ ”{” _ ,→keyboard_body _ ”}” 73 / ”if” _ ”(” _ expression _ ”)” _ ”{” _ keyboard_body _ ”}” 74 ; 75 76 keyboard_block <- ”{” _ body _ ”}”; 77 78 keyboard_shortcut <- keyboard_shortcut_key _ keyboard_arguments? _ ”:” _ expression 79 ; 80 81 list_comprehension <- expression ”for” _ ”$” _ namespaced _ ”in” _ value_expression _ ”..” _ ,→value_expression 82 / expression ”for” _ ”$” _ namespaced _ ”in” _ value_expression _ ”..” _ ,→value_expression _ ”if” value_expression 83 ; 84 85 keyboard_shortcut_key <- alphanumeric (_”+”_ alphanumeric)* (_ alphanumeric)* 86 / string_value (_ alphanumeric)* 87 / ”[” _ list_comprehension _ ”]” (alphanumeric _)* 88 / ”[” _ value_expression _ ”]” (alphanumeric _)* 89 ; 90 91 keyboard_arguments <- ”(” _ ( keyboard_single_argument ( _ arguments_separator _ ,→keyboard_single_argument )* )? _ ”)”; 92 93 keyboard_arguments_separator <- ',' / ';'; 94 95 keyboard_single_argument <- ”$” identifier; 96 // END Keyboard 97 98 python_expression <- ”@py” _ ”{” _ python_expression_body _ ”}”; 99 100 python_expression_body <- r”[^\}]*”; 101 102 python_statement <- ”@python” python_statement_body; 103 104 python_statement_body <- ( r”.*” r”\r?\n”? )*; 105 106 expression <- e music_expression; 107 108 music_expression <- sequence ( _ ”|” _ sequence )*; 109 110 sequence <- value_expression ( _ value_expression)*; 111 97 APÊNDICE A. GRAMÁTICA PEG 112 group <- ”(” _ expression _ ”)”; 113 114 block <- ”{” _ body _ ”}”; 115 116 variable <- ”$” namespaced; 117 118 function <- namespaced ”(” _ function_parameters _ ”)”; 119 120 function_parameters <- named_parameters 121 / positional_parameters ( _ parameters_separator _ named_parameters )? 122 / e 123 ; 124 125 positional_parameters <- !named_parameter expression ( _ parameters_separator _ ! ,→named_parameter expression )*; 126 127 named_parameters <- named_parameter ( _ parameters_separator _ named_parameter )*; 128 129 named_parameter <- identifier _ '=' _ expression; 130 131 parameters_separator <- ',' / ';'; 132 133 note <- note_pitch note_value?; 134 135 chord <- ”[” _ note_pitch chord_suffix _ ”]” note_value? 136 / ”[” ( _ note_pitch )+ _ ”]” note_value? 137 ; 138 139 chord_suffix <- 'm7' / 'M7' / 'dom7' / '7' / 'm7b5' / 'dim7' / 'mM7' 140 / '5' 141 / 'M' / 'm' / 'aug' / 'dim' / '+' 142 ; 143 144 rest <- ”r” note_value?; 145 146 note_value 147 <- ”/” _ integer 148 / integer _ ”/” _ integer 149 / integer 150 ; 151 152 note_pitch <- note_accidental _ note_pitch_raw; 153 154 note_accidental <- ”^” / ”^^” / ”__” / ”_” / ””; 155 98 156 note_pitch_raw <- r”[cdefgab]” ”'”* 157 / r”[CDEFGAB]” ”,”* 158 ; 159 160 modifier 161 <- r”[tT]” _ integer 162 / r”[vV]” _ integer 163 / r”[iI]” _ integer 164 / r”[lL]” _ note_value 165 / r”[sS]” _ integer _ ”/” _ integer 166 / r”[sS]” _ integer 167 / r”[oO]” _ integer 168 ; 169 170 instrument_modifier <- !”::” ”:” ( namespaced / ”?” ) _ sequence; 171 172 value_expression <- e binary_logic_operator_expression; 173 174 binary_logic_operator_expression <- binary_comparison_operator_expression _ (”and” / ”or”) _ ,→binary_logic_operator_expression 175 / binary_comparison_operator_expression 176 ; 177 178 binary_comparison_operator_expression <- binary_sum_operator_expression _ (”>=” / ”>” / ”==” / ,→”!=” / ”<=” / ”<” / ”isnot” / ”is” / ”in” / ”notin”) _ ,→binary_comparison_operator_expression 179 / binary_sum_operator_expression 180 ; 181 182 binary_sum_operator_expression <- binary_mult_operator_expression _ r”[+\-]” _ ,→binary_sum_operator_expression 183 / binary_mult_operator_expression 184 ; 185 186 binary_mult_operator_expression <- unary_operator_expression _ r”(\*\*|\*|\/)” _ ,→binary_mult_operator_expression 187 / unary_operator_expression 188 ; 189 190 unary_operator_expression <- ( ”not” / ”” ) _ expression_single; 191 192 expression_single <- expression_single_prefix ( ( _ property_accessor ) / property_call )*; 193 99 APÊNDICE A. GRAMÁTICA PEG 194 expression_single_prefix <- function_declaration / string_value / number_value / bool_value / ,→none_value / variable / function / keyboard_declaration / python_expression / ,→array_value / object_value / group / block / chord / note / rest / modifier / ,→instrument_modifier; 195 196 property_accessor <- ”::” _ ( identifier / ( ”[” _ expression _ ”]” ) ); 197 198 property_call <- ”(” _ function_parameters _ ”)”; 199 200 array_value <- ”@[” _ ”]” 201 / ”@[” _ expression ( _ array_separator _ expression )* _ ”]” 202 ; 203 204 array_separator <- ',' / ';'; 205 206 object_value <- ”@{” _ ”}” 207 / ”@{” _ object_value_item ( _ object_separator _ object_value_item )* _ ”}” 208 ; 209 210 object_value_item <- object_value_key _ '=' _ expression; 211 212 object_value_key <- identifier / float / integer / double_string / single_string; 213 214 object_separator <- ',' / ';'; 215 216 string_value <- double_string / single_string; 217 218 double_string <- ”\”” double_string_char* ”\””; 219 220 double_string_char 221 <- ”\\\”” 222 / ”\\\\” 223 / r”[^\”]” 224 ; 225 226 single_string <- ”'” single_string_char* ”'”; 227 228 single_string_char 229 <- ”\\'” 230 / ”\\\\” 231 / r”[^']” 232 ; 233 234 number_value <- float / integer; 235 100 236 bool_value <- ”true” / ”false”; 237 238 none_value <- ”none”; 239 240 float <- r”[0-9]+\.[0-9]+”; 241 242 integer <- r”[\-\+]?[0-9]+”; 243 244 namespaced <- ( identifier ”\\” )* identifier; 245 246 identifier <- r”[a-zA-Z\_][a-zA-Z0-9\_]*”; 247 248 alphanumeric <- r”[a-zA-Z0-9\_]*”; 249 250 _ <- r”[ \t\r\n]*”; 251 252 __ <- r”[ \t\r\n]+”; 253 254 e <- ””; 255 256 comment <- _ r”#[^\n]*”; Listagem A.1: Gramática PEG da linguagem Musikla 101 Apêndice B Gramática LALR 1start: [body] [python] 2 3?body: statement (”;” statement)* ”;”? 4 5python: ”@python” PYTHON_STATEMENTS 6 7?statement: voice_assignment 8|import 9| for_loop_statement 10 | while_loop_statement 11 | return_statement 12 | assignment 13 | expression 14 15 assignment: expression ASSIGNMENT_OP expression -> assignment 16 | expression (”,” expression)+ ”,”? ASSIGNMENT_OP expression -> multi_assignment 17 18 voice_assignment: VOICE_IDENTIFIER ”=” voice_assignment_body 19 20 voice_assignment_body: VOICE_CALL expression _RPAR -> voice_assignment_inherit 21 | expression -> voice_assignment_base 22 23 import: ”import” IDENTIFIER -> import_global 24 | ”import” STRING -> import_local 25 26 for_variables: VARIABLE_NAME ( _arguments_sep VARIABLE_NAME )* 27 28 for_loop_head: _FOR _LPAR for_variables ”in” expression_atom ”..” expression_atom _RPAR -> ,→for_loop_head_range 103 APÊNDICE B. GRAMÁTICA LALR 29 | _FOR for_variables ”in” expression_atom ”..” expression_atom -> ,→for_loop_head_range 30 | _FOR _LPAR for_variables ”in” expression _RPAR -> for_loop_head 31 | _FOR for_variables ”in” logic_op_expr -> for_loop_head 32 33 for_loop_statement: for_loop_head if_body 34 35 while_loop_statement: _WHILE logic_op_expr if_body 36 37 if_statement: _IF logic_op_expr if_body _ELSE logic_op_expr -> if_statement_else 38 // | _IF logic_op_expr if_body _ELSE if_statement -> if_statement_else 39 | _IF logic_op_expr if_body -> if_statement 40 41 if_body: ”then”? logic_op_expr 42 // | block 43 44 return_statement: _RETURN expression? 45 46 ?expression: sequence_expr 47 | sequence_expr (”|” sequence_expr)+ -> parallel 48 49 ?sequence_expr: logic_op_expr 50 | logic_op_expr logic_op_expr+ -> sequence 51 52 ?logic_op_expr: comparison_op_expr 53 | logic_op_expr ”and” comparison_op_expr -> and_logic_op 54 | logic_op_expr ”or” comparison_op_expr -> or_logic_op 55 56 ?comparison_op_expr: sum_op_expr 57 | comparison_op_expr ”>=” sum_op_expr -> gte_comparison_op 58 | comparison_op_expr ”>” sum_op_expr -> gt_comparison_op 59 | comparison_op_expr _EQ sum_op_expr -> eq_comparison_op 60 | comparison_op_expr ”!=” sum_op_expr -> neq_comparison_op 61 | comparison_op_expr ”<=” sum_op_expr -> lte_comparison_op 62 | comparison_op_expr ”<” sum_op_expr -> lt_comparison_op 63 | comparison_op_expr _ISNOT sum_op_expr -> isnot_comparison_op 64 | comparison_op_expr _IS sum_op_expr -> is_comparison_op 65 | comparison_op_expr _IN sum_op_expr -> in_comparison_op 66 | comparison_op_expr _NOTIN sum_op_expr -> notin_comparison_op 67 68 ?sum_op_expr: mult_op_expr 69 | sum_op_expr ”+” mult_op_expr -> sum_op 70 | sum_op_expr ”-” mult_op_expr -> sub_op 71 72 ?mult_op_expr: unary_op_expr 104 73 | mult_op_expr ”**” unary_op_expr -> pow_op 74 | mult_op_expr ”*” unary_op_expr -> mult_op 75 | mult_op_expr ”/” unary_op_expr -> div_op 76 77 ?unary_op_expr: expression_atom 78 | ”not” expression_atom -> negation 79 80 ?expression_atom: expression_atom ”::” accessor -> accessor 81 | if_statement 82 | function_declaration 83 | string_literal 84 | number_literal 85 | bool_literal 86 | none_literal 87 | variable 88 | function 89 | keyboard_declaration 90 | python_expression 91 | array_literal 92 | object_literal 93 | group 94 | block 95 | chord 96 | note 97 | rest 98 | modifier 99 | voice_modifier 100 101 accessor: FUNCTION_CALL function_call -> method_call 102 | IDENTIFIER -> property_accessor 103 | _LSQR expression _RSQR -> index_accessor 104 | _LSQR expression _INDEX_CALL function_call -> index_call 105 106 python_expression: ”@py” _LBRAK PYTHON_EXPR _RBRAK 107 108 // Function Calls 109 function: FUNCTION_CALL function_call 110 111 function_call: [function_parameters] _RPAR -> function_call 112 | [function_parameters] _GROUP_CALL function_call -> function_call_chain 113 114 function_parameters: parameter (_parameters_sep parameter)* 115 116 ?parameter: named_parameter | positional_parameter 117 105