Parte S: Tamanho do bloco no external

Parte S: Tamanho do bloco no external

por Andre Jucovsky Bianchi -
Número de respostas: 5

Oi, gente.

Um aluno me escreveu o seguinte:

Estou com problemas na Parte S do EP3.

Meu EP sintetiza normalmente arquivos com N = 64. Meu problema está sendo para N maiores que esse valor pois t_float *out guarda no máximo 64 valores.

Existe alguma técnica que devo aplicar para que seja possivel armazenar mais que 64 valores no vetor out? Ou algum jeito de aumentar o tamanho desse vetor?

Acho que a questão aqui é entender o processamento digital em blocos no PD.

Usando o objeto [block~], é possível determinar o tamanho do bloco e do overlap utilizado pelo PD no processamento digital. Isso significa que os objetos que emitem e recebem sinais vão ter sua entrada e saída realizada em blocos de tamanho fixo, com possível sobreposição entre os blocos. Isso causa um comportamento diferente somente para os objetos com ~.

Um bom exemplo é a diferença de funcionamento do [tabread4] para o [tabread4~]. Enquanto o primeiro, controlado por um [metro], por exemplo, possui um comportamento invariável, o segundo tem sua entrada, saída e período máximo de computação determinados pelo tamanho do bloco e da sobreposição.

Nesse sentido, a proposta na parte de Análise é, de certa forma, subverter a infraestrutura de tempo real do PD e utilizar o tamanho dos blocos e da sobreposição para controlar a leitura e processamento do arquivo de entrada, mas sem o compromisso de produzir amostras para serem tocadas em tempo real. Assim, o processamento da análise é feito em tempo real, mas não se produz nenhum sinal audível. (No exemplo I07 as duas coisas são feitas ao mesmo tempo.)

Na escrita do external, o tamanho do bloco vai influenciar também o funcionamento da função perform(). Essa função, adicionada ao escalonador do PD, é chamada a cada ciclo de DSP e recebe vetores do tamanho do bloco definido. No seu caso, o vetor de entrada e saída tem 64 amostras porque esse é o tamanho do bloco padrão do PD.

Agora, a idéia da parte de Síntese é que ela possa ser feita com sucesso independentemente do tamanho do bloco definido no PD. O número de amostras correspondentes a um intervalo sonoro de mesma frequência depende do valor de α, que pode ser alterado durante a síntese. A idéia, então, é manter o controle do número de amostras geradas independente do tamanho do vetor de amostras de saída. Em cada momento que a função perform() for chamada, é necessário continuar a síntese do ponto onde ela parou na última chamada, independente da frequência de chamada da função.

Em outras palavras, os endereços de amostras apontados por out devem ser utilizados somente para saída de amostras, não para armazenamento e controle da produção do sinal. Realize os cálculos independentemente do tamanho do bloco e vá jogando os resultados para a saída a medida em estiverem prontos.

Certo?

Em resposta à Andre Jucovsky Bianchi

Re: Parte S: Tamanho do bloco no external

por Flávio Luiz Schiavoni -

Eu vejo assim, me corrijam se eu estiver errado.

Um dos parâmetros que o perform() recebe é o tamanho do bloco do PD. Seu external não deve assumir que seja 64, apesar deste ser o padrão do PD. O perform() recebe um parâmetro n que é o tamanho do bloco. Vamos supor que seja n = 64.

A primeira informação em seu arquivo .pv é o tamanho do bloco e o overlap. Imagninemos que seja N = 1024 e M=1. Isto nos daria um conjunto de 511 osciladores que devem soar por 1024 amostras. Isto significa que vc deve rodar o perform() 16 vezes para cada conjunto de osciladores (16 x 64 = 1024 ou 1024/64 = 16 vezes).

No caso de termos overlap, ou seja, N = 1024 e M = 2, cada conjunto de osciladores deverá soar por N/M, ou seja, 512 amostras. De novo, a conta é exata. Terás de executar 8 vezes cada conjunto de oscilador por cada execução do perform(). Colocando o M na conta, temos (N/M)/n = qtde_de_perform. Neste exemplo, (1024/2)/64 = 8.

E assim podemos pensar em cada conjunto de oscilador, independente do tamanho dele, do tamanho do N, do M e do n. Felizmente tudo funciona com potências de 2 facilitando sempre a nossa conta. Eles serão sempre, de certa forma, múltiplos. Coloque um contador que soma a quantidade de amostras que já foram processadas somando de n em n. Ao chegar no valor desejado vc troca o conjunto de osciladores e boa. Terás a síntese.

Indo além desta explicação, passamos a incluir nesta conta o alfa. O alfa vai alterar a duração da sua síntese. Isto significa que cada conjunto de osciladores soará não mais por (N/M)/n blocos mas por mais ou menos que isto dependendo do valor de alfa ser maior que 1 ou menor. Coloca o alfa multiplicando na conta e terás a quantidade de vezes que cada conjunto de amostras deverá soar. A conta vira alfa.((N/M)/n). Isto implica em mudar o conjunto de osciladores no meio do perform(). O Marcelo simplificou tudo ao pedir que o alfa fosse mudado apenas a cada bloco e não durante o bloco.
Isto é uma dica de onde checar o seu contador de amostras já processadas.

Espero não estar sendo spoiler. E, apesar de estas contas todas estarem explicadas na proposta do EP, espero que meu comentário ajude. Eu precisei de bastante ajuda do André para entender isto tudo.

Abraços

Em resposta à Flávio Luiz Schiavoni

Re: Parte S: Tamanho do bloco no external

por Marcelo Queiroz -

 

Flávio, acho que você complicou demais o problema. Se isto funcionou para a sua solução, ótimo, mas não era necessário pensar assim. Em particular sua conta para calcular qtde_de_perform não tem nada a ver com a forma como eu implementei. Não fiz nada somando de n em n. E a conta com o alfa multiplicando ou está errada ou eu não entendi também (provavelmente não entendi, pois na minha implementação não há um conjunto de amostras a serem repetidas). E o alfa não pode ser mudado a cada bloco, mas apenas a cada evento definido por uma linha do arquivo de entrada! Você deve ter complicado substancialmente o EP em relação ao enunciado...

Acho muito mais fácil pensar da seguinte maneira: para cada linha do arquivo .pv você tem que produzir exatamente D=round(N/(M*alfa)) amostras. Ao iniciar o processamento desta linha, inicialize um contador e vá contando cada amostra produzida na saída, independentemente do bloco de processamento do Pd. Quanto o contador chegar a D, É pra ler uma linha nova do arquivo .pv. Isso pode facilmente acontecer no meio de uma chamada do process(), já que o alfa é qualquer, por isso é importante contabilizar as amostras independentemente do tamanho do bloco.

Na produção do sinal de saída, o que você precisa é fazer o laço do process() produzir n amostras, onde n é o tamanho do bloco. Para isso é necessário conhecer os índices de leitura de cada oscilador, para atualizá-los de acordo com os Delta(j). Antes de produzir cada amostra você testa pra ver se contador<D. Se for, faz um laço para somar a saída de cada um dos K osciladores. Se não for, lê uma linha do arquivo .pv e reinicializa todos os parâmetros que variam de evento para evento (alfa, beta, D, contador=0 e vetor de Deltas(j)).

Abraços,

Marcelo

Em resposta à Marcelo Queiroz

Re: Parte S: Tamanho do bloco no external

por Flávio Luiz Schiavoni -

Olá Marcelo

Creio que nossas implementações devam estar parecidas. Talves eu não consegui me expressar direito no texto. Para evitar lidar com leitura de arquivos no meio do processamento eu leio todo ele antes e armazeno em um array. Por isto minha explicação lida com conjunto de osciladores e não com linhas do arquivo. No fim, sei que é a mesma coisa.

Não há um conjunto de amostras a serem repetidas mas um conjunto de osciladores que gerarão amostras por um determinado período de tempo. O que pode ser necessário é gerar mais ou menos amostras com o mesmo conjunto de osciladores. Eu não faço esta conta com quantidade de tempo mas sim quantas amostras eu devo gerar com cada conjunto de osciladores.

Respondendo também a mensagem do André abaixo, eu fui resolvendo o problema aos poucos. Na primeira implementação eu trabalhei apenas com arquivos sem overlap (M=1) e sem alfa (a=1).Neste caso eu contava de n em n. O segundo passo foi adicionar o overlap (M != 1) e então foi necessário contar de um em um. O terceiro passo foi colocar o parâmetro alfa. Resolver passo a passo me ajudou a implementar e ter certeza do resultado que esperava.

Sei que o que foi pedido é que o alfa variasse a cada linha do arquivo. O ajuste mais fino que consegui fazer com o alfa foi calculá-lo a cada bloco de amostra do PD, sem comprometer o resultado final.

Acho que é isto. Desculpem-me se acabei atrapalhando em vez de ajudar.

Em resposta à Flávio Luiz Schiavoni

Re: Parte S: Tamanho do bloco no external

por Andre Jucovsky Bianchi -

Opa, Flávio, só pra esclarecer um ponto, acho que eu refrasearia uma parte do que você disse assim:

"Isto nos daria um conjunto de 511 osciladores e duas linhas com parâmetros para estes osciladores (uma com valores de amplitude e outra com valores de frequência) para cada 1024 amostras do sinal de entrada."

Isso não quer dizer que os osciladores devam soar por exatamente 1024 amostras na Síntese. Na verdade, o número de amostras geradas por um conjunto de parâmetros para os 511 osciladores vai variar em função de α.

Como o Marcelo disse abaixo, dá pra "desacoplar" o período (em amostras) do ciclo DSP da geração das amostras para cada intervalo com período determinado por α.

Em resposta à Andre Jucovsky Bianchi

Re: Parte S: Tamanho do bloco no external

por Andre Jucovsky Bianchi -

Arriscando tornar a discussão ainda mais complicada, aproveito pra compartilhar alguns testes que andei fazendo com o PD.

Em anexo, envio um patch que testa o funcionamento do [block~] com diferentes valores de N e M. O objetivo dele é gerar um vetor de tamanho N com valores de 0 até N-1. O interessante é que para gerar corretamente, é necessário passar para o objeto [line~] exatamente o período de um ciclo DSP considerando a sobreposição, ou seja, N/(RM) segundos. Isto evidencia que o ciclo DSP tem seu período alterado pelo fator de sobreposição.

Além disso, o patch também calcula o tempo entre dois ciclos DSP (na marra, com [timer]), o valor teórico do período de um ciclo (dado por N/(RM)) e o número de ciclos DSP por segundo.

Só que tem alguns comporamentos que são um pouco estranhos. Supostamente, o tempo entre dois ciclos DSP deveria ser sempre igual ao valor teórico do período de um ciclo. Mas, pelos testes, isso só acontece quando N/M >= 64. Se N/M é menor que 64, continua funcionando a geração do vetor como descrevi, mas o tempo medido entre dois ciclos fica sempre em 1,415 milissegundo (exatamente 64/44100).

Dando uma olhada no fonte do PD, descobri que as contas de ajuste dos vetores nos inlets e outlets (por conta de tamanho de bloco, sobreposição, downsample ou upsample) são sempre feitas levando em conta os mesmos valores da "janela pai" da janela que estamos trabalhando. O PD cria novos vetores (ou pega emprestado vetores "fantasma") para acomodar as amostras se a saída de sinal de um objeto possui configurações diferentes da entrada de sinal de outro objeto. Além disso, a janela principal do PD sempre possui blocos de 64 amostras, sem fatores de sobreposição, upsample ou downsample. Isso de alguma forma limita o período do ciclo DSP de alguma forma que ainda não consegui descobrir.

Pra não deixar tão no ar, colo aqui um comentários do fonte que esclarece um pouco a questão. No arquivo d_ugen.c, linhas 74-102:

/* ---------------------------- block~ ----------------------------- */

/* The "block~ object maintains the containing canvas's DSP computation,
calling it at a super- or sub-multiple of the containing canvas's
calling frequency.  The block~'s creation arguments specify block size
and overlap.  Block~ does no "dsp" computation in its own right, but it
adds prolog and epilog code before and after the canvas's unit generators.

A subcanvas need not have a block~ at all; if there's none, its
ugens are simply put on the list without any prolog or epilog code.

Block~ may be invoked as switch~, in which case it also acts to switch the
subcanvas on and off.  The overall order of scheduling for a subcanvas
is thus,

inlet and outlet prologue code (1)
block prologue (2)
the objects in the subcanvas, including inlets and outlets
block epilogue (2)
outlet epilogue code (2)

where (1) means, "if reblocked" and  (2) means, "if reblocked or switched".

If we're reblocked, the inlet prolog and outlet epilog code takes care of
overlapping and buffering to deal with vector size changes.  If we're switched
but not reblocked, the inlet prolog is not needed, and the output epilog is
ONLY run when the block is switched off; in this case the epilog code simply
copies zeros to all signal outlets.
*/

Depois que as configurações são definidas pelo [block~], é o código em g_io.c que cuida de computar o prólogo e epílogo de cada inlet e outet.

Vale a pena dar uma olhada. Quando eu tiver uma resposta exata sobre o porquê do limite inferior para o período do ciclo DSP medido, que em alguns casos é diferente do que realmente acontece na execução, escrevo aqui.

PS: para o patch anexo funcionar, não esqueça de selecionar a caixinha para ativar o [switch~].