AudioLazy é um projeto open source que fornece ferramentas de processamento digital de sinal (DSP) expressivas e em tempo real para Python. Ele permite análise, síntese e processamento de áudio com código Python de forma concisa e facilita a prototipagem de aplicações de áudio interativas e em tempo real.
(2013-10-02) [PythonBrasil] Compatibilidade entre Python 2 e 3
Audio DSP em Python com AudioLazy
1. AudioLazy
DSP (DSP (Digital Signal ProcessingDigital Signal Processing) expressivo e) expressivo e
em tempo real para o Pythonem tempo real para o Python
Projeto open source (GPLv3)Projeto open source (GPLv3)
http://pypi.python.org/pypi/audiolazyhttp://pypi.python.org/pypi/audiolazy
Copyright (C) 2012-2013Copyright (C) 2012-2013
Danilo de Jesus da Silva BelliniDanilo de Jesus da Silva Bellini
danilo [dot] bellini [at] gmail [dot] comdanilo [dot] bellini [at] gmail [dot] com
3. AudioLazy
● DSP (Digital Signal Processing) para áudio
– Análise
● MIR (Music Information Retrieval)
– Síntese
– Processamento
● Expressividade de código
– Facilita prototipação, simulação
● Tempo real (latência de 35ms é possível)
– Possibilita uso em aplicações finais
● 100% Python
– Multiplataforma
4. Justificativa
● Demanda e insatisfação com código existente
– Sustainable Software for Audio and Music Research
● ISMIR 2012 (Tutorial)
● DAFx 2012 (Tutorial)
– Software Carpentry
● Ausência de código fonte disponível
– Algoritmo de Klapuri (2008)
● Base para trabalhos futuros
5. Linguagem
Critério MatLab Octave PureData Python NumPy
Amostras Sim Sim Depende Sim Sim
Blocos Possível Possível Sim Possível Possível
Álgebra linear Sim Sim Depende Possível Sim
DSP Sim Sim Sim Possível Sim
Tempo real Não Não Sim Possível Não
Heterogeneidade Não Não Não Sim Possível
Avaliação tardia Não Não Depende Sim Não
Funções de ordem
superior
Não Não Não Sim Não
Orientação a objetos Possível Possível Não Sim Sim
Linguagem de uso
geral
Não Não Não Sim Sim
Documentação Sim Sim Sim Sim Sim
Licença Proprietária GNU GPL
BSD
Modificada
PSFL BSD
Preço US$99.00 Gratuito Gratuito Gratuito Gratuito
Código fonte (Fechado) C++ C C, Python
C, Fortran,
Python
6. Outras linguagens
● CAS (Computer
Algebra System)
– Wolfram Mathematica
– SymPy
● C, C++, Java
– Estáticas
– Imperativo
– S/ ênfase em ser
expressivo
● DSL (Domain Specific
Language)
– FAUST
– CSound
– SuperCollider
– ChunK
● Pacotes
– YAAFE
– MARLib
8. Testes com oráculos
● 80 c/ o scipy.signal.lfilter
● 64 c/ o subpacote de otimização do SciPy
● 2 c/ o NumPy
import pytest
p = pytest.mark.parametrize
from scipy.signal import lfilter
from audiolazy import ZFilter, almost_eq
class TestZFilterScipy(object):
@p("a", [[1.], [3.], [1., 3.], [15., -17.2], [-18., 9.8, 0., 14.3]])
@p("b", [[1.], [-1.], [1., 0., -1.], [1., 3.]])
@p("data", [range(5), range(5, 0, -1), [7, 22, -5], [8., 3., 15.]])
def test_lfilter(self, a, b, data):
filt = ZFilter(b, a) # Cria um filtro com a AudioLazy
expected = lfilter(b, a, data).tolist() # Aplica o filtro com o SciPy
assert almost_eq(filt(data), expected) # Compara os resultados
import pytest
p = pytest.mark.parametrize
from scipy.signal import lfilter
from audiolazy import ZFilter, almost_eq
class TestZFilterScipy(object):
@p("a", [[1.], [3.], [1., 3.], [15., -17.2], [-18., 9.8, 0., 14.3]])
@p("b", [[1.], [-1.], [1., 0., -1.], [1., 3.]])
@p("data", [range(5), range(5, 0, -1), [7, 22, -5], [8., 3., 15.]])
def test_lfilter(self, a, b, data):
filt = ZFilter(b, a) # Cria um filtro com a AudioLazy
expected = lfilter(b, a, data).tolist() # Aplica o filtro com o SciPy
assert almost_eq(filt(data), expected) # Compara os resultados
9. Comparação de números em ponto
flutuante (IEEE 754)
● Valor absoluto (limiar “l”)
● Comparação pelo número de bits de mantissa (“t”
bits de tolerância para “s” bits de mantissa)
● Implementado na audiolazy.lazy_misc
– almost_eq_diff
– almost_eq
∣a−b∣≤l
∣a−b∣≤2(t − s−1)∣a+b∣
10. Parte 2
Síntese e processamento em tempo realSíntese e processamento em tempo real
11. “Hello world” em áudio
● Tocar uma senóide
– Console interativo
– Script
from audiolazy import *
rate = 44100
s, Hz = sHz(rate)
th = AudioIO().play(sinusoid(440 * Hz), rate=rate)
from audiolazy import *
rate = 44100
s, Hz = sHz(rate)
th = AudioIO().play(sinusoid(440 * Hz), rate=rate)
from audiolazy import *
rate = 44100
s, Hz = sHz(rate)
with AudioIO(True) as player:
player.play(sinusoid(440 * Hz), rate=rate)
from audiolazy import *
rate = 44100
s, Hz = sHz(rate)
with AudioIO(True) as player:
player.play(sinusoid(440 * Hz), rate=rate)
Multithread!
12. Síntese
● Aditiva
– Senóide (sinusoid)
– Table-lookup
● Senóide (sin_table)
● Dente de serra
(saw_table)
● AM
– Modulação em amplitude
– Modulação em anel
● FM
– Exemplo em wxPython
● Subtrativa
– Karplus-Strong
● Examplo com os corais
de J. S. Bach (Music21)
● Personalizada
– Construtor da classe
Stream
– Decorador tostream
14. Armazenamento: Estratégias para
avaliação de expressões
● Antecipada (eager)
– Chamada por valor (call by value)
– Avaliação ocorre antes da chamada
● Tardia (lazy)
– Chamada por nome (call by name)
● Reavaliação a cada chamada
– Chamada por necessidade (call by need)
● Memoize / Cache
● Há quem considere essa a única forma de avaliação
“preguiçosa” (lazy)
15. Avaliação tardia
● Evita cálculos desnecessários
● Estruturas de tamanho indefinido
– Potencialmente infinitas (e.g. contador)
● Fluxo de controle como abstração
In [1]: def gerador_123():
...: val = 1
...: while True:
...: yield val
...: val = val + 1 if val < 3 else 1
...:
In [2]: gerador_123
Out[2]: <function __main__.gerador_123>
In [3]: gerador_123()
Out[3]: <generator object gerador_123 at 0x...>
In [1]: def gerador_123():
...: val = 1
...: while True:
...: yield val
...: val = val + 1 if val < 3 else 1
...:
In [2]: gerador_123
Out[2]: <function __main__.gerador_123>
In [3]: gerador_123()
Out[3]: <generator object gerador_123 at 0x...>
In [4]: sinal = gerador_123()
In [5]: sinal.next()
Out[5]: 1
In [6]: sinal.next()
Out[6]: 2
In [7]: sinal.next()
Out[7]: 3
In [8]: sinal.next()
Out[8]: 1
In [4]: sinal = gerador_123()
In [5]: sinal.next()
Out[5]: 1
In [6]: sinal.next()
Out[6]: 2
In [7]: sinal.next()
Out[7]: 3
In [8]: sinal.next()
Out[8]: 1
Desempenho!
16. Para áudio, o que precisamos?
● Sequência de símbolos
– Símbolos são números, normalmente
● Tempo real
– Dados (elementos da sequência) inexistentes em
tempo de compilação
– Duração indefinida
– Não é necessário computar tudo para começar a
apresentar o resultado
Avaliação tardia!
17. Classe Stream
● Representa fluxo de informação (áudio)
● Iterável heterogêneo com operadores (baseado no
NumPy) e avaliação tardia
● Ausência de índices
– Limite de representação inteira (32 bits estouraria em
27:03:12)
In [1]: from audiolazy import Stream
In [2]: dados = Stream(5, 7, 1, 2, 5, 3, 2) # Periódico
In [3]: dados2 = Stream(0, 1) # Idem
In [4]: (dados + dados2).take(15)
Out[4]: [5, 8, 1, 3, 5, 4, 2, 6, 7, 2, 2, 6, 3, 3, 5]
In [1]: from audiolazy import Stream
In [2]: dados = Stream(5, 7, 1, 2, 5, 3, 2) # Periódico
In [3]: dados2 = Stream(0, 1) # Idem
In [4]: (dados + dados2).take(15)
Out[4]: [5, 8, 1, 3, 5, 4, 2, 6, 7, 2, 2, 6, 3, 3, 5]
18. Classe Stream
● Métodos, atributos e propriedades são aplicados
elemento a elemento
– Exceto “take”, “blocks” e outros da própria classe
Stream
● Finito ou de finalização indeterminada
In [5]: Stream([2, 3, 4]).take(5) # Lista de entrada
Out[5]: [2, 3, 4]
In [6]: Stream(2, 3, 4).take(5) # Números de entrada
Out[6]: [2, 3, 4, 2, 3]
In [7]: Stream(*[2, 3, 4]).take(5) # Lista com "*"
Out[7]: [2, 3, 4, 2, 3]
In [8]: (2 * Stream([1 + 2j, -3j, 7]).real).take(inf)
Out[8]: [2.0, 0.0, 14]
In [5]: Stream([2, 3, 4]).take(5) # Lista de entrada
Out[5]: [2, 3, 4]
In [6]: Stream(2, 3, 4).take(5) # Números de entrada
Out[6]: [2, 3, 4, 2, 3]
In [7]: Stream(*[2, 3, 4]).take(5) # Lista com "*"
Out[7]: [2, 3, 4, 2, 3]
In [8]: (2 * Stream([1 + 2j, -3j, 7]).real).take(inf)
Out[8]: [2.0, 0.0, 14]
19. Decorador tostream:
Geradores convertidos em Stream
● Função = Decorador(Função)
In [1]: from audiolazy import tostream
In [2]: @tostream
...: def impulse():
...: yield 1
...: while True:
...: yield 0
...:
In [3]: impulse # De fato, uma função
Out[3]: <function __main__.impulse>
In [4]: impulse() # Devolve um objeto Stream
Out[4]: <audiolazy.lazy_stream.Stream at 0x30824d0>
In [5]: impulse().take(5)
Out[5]: [1, 0, 0, 0, 0]
In [6]: (impulse() + 1).take(5) # Outro objeto instanciado
Out[6]: [2, 1, 1, 1, 1]
In [1]: from audiolazy import tostream
In [2]: @tostream
...: def impulse():
...: yield 1
...: while True:
...: yield 0
...:
In [3]: impulse # De fato, uma função
Out[3]: <function __main__.impulse>
In [4]: impulse() # Devolve um objeto Stream
Out[4]: <audiolazy.lazy_stream.Stream at 0x30824d0>
In [5]: impulse().take(5)
Out[5]: [1, 0, 0, 0, 0]
In [6]: (impulse() + 1).take(5) # Outro objeto instanciado
Out[6]: [2, 1, 1, 1, 1]
20. Processamento em bloco
● Stream.blocks(size, hop)
– Qualquer salto (hop) positivo
– Se mudar a saída, a mudança persistirá na próxima
saída quando hop < size
● Saídas são a mesma fila circular implementada como
collections.deque
In [1]: data = Stream([1, 2, 3])
In [2]: blks = data.blocks(size=2, hop=1)
In [3]: [list(blk) for blk in blks]
Out[3]: [[1, 2], [2, 3]]
In [1]: data = Stream([1, 2, 3])
In [2]: blks = data.blocks(size=2, hop=1)
In [3]: [list(blk) for blk in blks]
Out[3]: [[1, 2], [2, 3]]
23. Filtros LTI
(Lineares e invariantes no tempo)
““Digital signal processing is mainlyDigital signal processing is mainly
based on linear time-invariantbased on linear time-invariant
systems.systems.””
(Dutilleux, Dempwolf, Holters e Zölzer(Dutilleux, Dempwolf, Holters e Zölzer
DAFx, segunda edição, capítulo 4, p. 103)DAFx, segunda edição, capítulo 4, p. 103)
32. Ainda sobre frequência fundamental e
periodicidade ...
● SDF (Square Difference Function)
● ACF (Autocorrelação)
– Inverso à AMDF e SDF (aqui queremos o maior valor)
● MPM (McLeod e Wyvill)
– Utilizar ACF para “normalizar” SDF
DFT ?!?
45. AbstractOperatorOverloaderMeta
● Metaclasse
– Classe cujas instâncias são classes
● Abstrata
– Classe com recursos especificados porém sem
implementação
● Sobrecarga massiva de operadores:
– Binários
– Binários reversos
– Unários
46. Objeto window
Um dicionário de estratégias
In [1]: from audiolazy import window
In [2]: window # Vejamos as estratégias disponíveis
Out[2]:
{('bartlett',): <function audiolazy.lazy_analysis.bartlett>,
('blackman',): <function audiolazy.lazy_analysis.blackman>,
('hamming',): <function audiolazy.lazy_analysis.hamming>,
('hann', 'hanning'): <function audiolazy.lazy_analysis.hann>,
('rectangular', 'rect'): <function audiolazy.lazy_analysis.rectangular>,
('triangular', 'triangle'): <function audiolazy.lazy_analysis.triangular>}
In [3]: window["rect"](3) # Obtém a estratégia, chamando com 1 argumento
Out[3]: [1.0, 1.0, 1.0]
In [4]: window.triangle(3) # Idem, mas feito com outra sintaxe (dicionário)
Out[4]: [0.5, 1.0, 0.5]
In [5]: hm_wnd = window.hamming # Referenciando fora do dicionário
In [6]: hm_wnd # Esta estratégia é uma função comum
Out[6]: <function audiolazy.lazy_analysis.hamming>
In [1]: from audiolazy import window
In [2]: window # Vejamos as estratégias disponíveis
Out[2]:
{('bartlett',): <function audiolazy.lazy_analysis.bartlett>,
('blackman',): <function audiolazy.lazy_analysis.blackman>,
('hamming',): <function audiolazy.lazy_analysis.hamming>,
('hann', 'hanning'): <function audiolazy.lazy_analysis.hann>,
('rectangular', 'rect'): <function audiolazy.lazy_analysis.rectangular>,
('triangular', 'triangle'): <function audiolazy.lazy_analysis.triangular>}
In [3]: window["rect"](3) # Obtém a estratégia, chamando com 1 argumento
Out[3]: [1.0, 1.0, 1.0]
In [4]: window.triangle(3) # Idem, mas feito com outra sintaxe (dicionário)
Out[4]: [0.5, 1.0, 0.5]
In [5]: hm_wnd = window.hamming # Referenciando fora do dicionário
In [6]: hm_wnd # Esta estratégia é uma função comum
Out[6]: <function audiolazy.lazy_analysis.hamming>
47. Polinômios
● Necessário para os filtros lineares
● Baseados em dicionário
– Memória
– Expoente negativo (Laurent)
– Expoente fracionário (soma de potências)
● Coeficientes podem ser objetos Stream, símbolos
do SymPy, etc.
48. Filtros
● Implementação direta I
– Evita multiplicação por 1
– Não cria os termos com coeficiente nulo
– Ainda ineficiente quando longo (e.g. FIR, comb)
● JIT (Just in Time)
– Cada filtro é criado e compilado em tempo de
execução
– Permite filtros variantes no tempo gerais e eficientes
50. Módulos
Nome Descrição
lazy_analysis Análise de áudio
lazy_auditory Modelagem do aparato auditivo humano periférico
lazy_core Núcleo com as três classes de fundamentação do pacote
lazy_filters Filtros
lazy_io Gravação e reprodução de áudio (via PyAudio), multi-thread
lazy_itertools Conteúdo da itertools “decorado” ou adaptado para objetos Stream
lazy_lpc Codificação linear preditiva (LPC)
lazy_math Funções matemáticas para uso em iteráveis com manutenção de tipo
lazy_midi Representação MIDI e relações entre nota e frequência (altura)
lazy_misc Diversas ferramentas de uso geral e constantes
lazy_poly Polinômios
lazy_stream Definição da classe Stream e derivadas
lazy_synth Pequeno sintetizador
51. Classes
Nome Bases (herança) Módulo Descrição
AudioIO object lazy_io Reprodutor/gravador de áudio
AudioThread threading.Thread lazy_io Thread representando objetos sendo reproduzidos
LinearFilterProperties object lazy_filters Mixin com conversores de propriedades de filtros lineares
LinearFilter LinearFilterProperties lazy_filters Filtro linear
ZFilter LinearFilter lazy_filters Filtro linear representado por equações em Z
FilterList list, LinearFilterProperties lazy_filters Lista de filtros
CascadeFilter FilterList lazy_filters Filtros em cascata
ParallelFilter FilterList lazy_filters Filtros em paralelo
Poly object lazy_poly Polinômios, polinômios de Laurent, soma de potências
TableLookup object lazy_synth Sintetizador por consulta à tabela
MultiKeyDict dict lazy_core Dicionário multi-chave
StrategyDict MultiKeyDict lazy_core Dicionário de estratégias
StrategyDictInstance StrategyDict lazy_core Uma classe para cada dicionário de estratégias
Stream collections.Iterable lazy_stream Iterável com operadores elemento a elemento e avaliação tardia
ControlStream Stream lazy_stream Stream que devolve um valor controlável, permitindo
interatividade
Streamix Stream lazy_stream Misturador (mixer) de objetos Stream baseado na temporização
do MIDI
StreamTeeHub Stream lazy_stream Gerenciador de cópias de Stream
MemoryLeakWarning Warning lazy_stream Número de usos de um StreamTeeHub menor que o especificado
ParCorError ZeroDivisionError lazy_lpc Erro ao tentar obter coeficientes PARCOR
52. Diagrama de classes
(Relações internas)
● Classes “centrais”
– AbstractOperatorOverloaderMeta
– Stream
● StrategyDict possui instâncias
– Diagrama omite informações importantes
53. Dicionários de estratégias presentes
na AudioLazy
Nome Estratégias Módulo Descrição
window 6 lazy_analysis Funções de janelamento ou
apodização
envelope 3 lazy_analysis Filtros de obtenção de envoltória
dinâmica
maverage 3 lazy_analysis Criador de filtros de média móvel
erb 2 lazy_auditory Largura de banda equivalente
retangular (ERB)
gammatone 3 lazy_auditory Filtros gammatone
comb 3 lazy_filters Filtros comb
resonator 4 lazy_filters Ressonadores
lowpass 2 lazy_filters Passa-baixas
highpass 1 lazy_filters Passa-altas
lpc 5 lazy_lpc Codificação linear preditiva (LPC)
54. Documentação
● Docstrings: documentação no código
– Uso em ambientes interativos
– reStructuredText
– Organização em seções
● Spyder
● Sphinx
– Conversão automática do sistema de seções para o formato do
Sphinx
– Muitos formatos: LaTeX (PDF, DVI, PS), ePUB, HTML, TexInfo, etc.
● Apresentação, instruções de instalação e exemplos básicos
– Integração com MatPlotLib, Music21, wxPython, Tkinter, etc.
55. Exemplos de implementação de partes
da AudioLazy
def levinson_durbin(acdata, order):
def inner(a, b):
return sum(acdata[abs(i-j)] * ai * bj
for i, ai in enumerate(a.numlist)
for j, bj in enumerate(b.numlist)
)
A = ZFilter(1)
for m in range(1, order + 1):
B = A(1 / z) * z ** -m
A -= inner(A, z ** -m) / inner(B, B) * B
return A
def levinson_durbin(acdata, order):
def inner(a, b):
return sum(acdata[abs(i-j)] * ai * bj
for i, ai in enumerate(a.numlist)
for j, bj in enumerate(b.numlist)
)
A = ZFilter(1)
for m in range(1, order + 1):
B = A(1 / z) * z ** -m
A -= inner(A, z ** -m) / inner(B, B) * B
return A