É explicado neste texto como sinais digitalizados de voz podem ser processados com a finalidade de modificar sua duração (deformação temporal) e/ou a frequência fundamental da voz, sem alterar as formantes da fala (as formantes da fala correspondem às frequências de ressonância do trato vocal do orador).

Para que o texto guarde um aspecto prático e simples de entender e reproduzir, é considerado apenas um sinal de áudio como ilustração de todos os procedimentos, e esses procedimentos serão explicados e implementados com linguagem do Scilab (www.scilab.org).

Passos iniciais:

a) Instalar o Scilab no computador

b) “Baixar” o arquivo ‘ai.wav’ de audio no site https://www.sites.google.com/site/pdsdelufs/

c) Aponte o Scilab para a pasta onde o arquivo ‘ai.wav’ foi colocado e ‘carreque’ as amostras do sinal no ambiente do programa. Por exemplo, se a pasta for  ‘C:\temp’, os comandos para apontar o Scilab para essa pasta e carregar o sinal são:

–> chdir(‘C:\temp’);

–> [s,fa]=wavread(‘ai.wav’);

Note que a variável fa representa a frequência de amostragem do sinal

–> fa //tecle enter depois da variável para ver seu conteúdo

fa =

11025.

O sinal também pode ser visto assim, mas por se tratar de um vetor com muitas amostras, é mais conveniente inspecioná-lo de outras maneiras. Por exemplo:

–> size(s)

ans =

1. 11810.

O que indica que a ”matriz” s possui 1 linha e 11810 colunas, ou seja, trata-se de um vetor linha;

–> plot(s)

Nesse gráfico, a direção horizontal representa o tempo discreto (ou o contador de amostras), e a direção vertical representa a pressão do ar no microfone que coletou o sinal, a menos de um fator de escala (i.e. não sabemos ao certo a unidade física da medida, mas assumimos que ela representa a pressão instantânea multiplicada por um fator de escala).

Caso o computador de trabalho também esteja munido de conversor D/A (numa placa de áudio, por exemplo), o sinal também pode ser escutado:

–> sound(s,fa)

Detecção dos cruzamentos ascendentes por zero e separação dos “grãos” de som

O ponto fundamental para a simplicidade do tipo de processamento explicado aqui é a detecção de cruzamentos por zero em direção ascendente (derivada positiva). Na linguagem do Scilab, os instantes em que esses pontos acontecem podem ser obtidos assim:

cz=find(s(2:$)>=0 & s(1:$-1)<0);

Mas como há muitos cruzamentos muito próximos uns dos outros, podemos impor um intervalo de tempo mínimo entre os cruzamentos. Por exemplo, podemos impor que os cruzamentos devem, no mínimo, guardar um intervalo de 20 ms entre eles. O código para impor essa restrição é:

Limiar=round(0.02*fa);

czLimiar(1)=cz(1);

for k=2:length(cz),

if cz(k)-czLimiar($)>=Limiar,

czLimiar=[czLimiar cz(k)];

end,

end

Chamaremos de grãos sonoros os segmentos de sinais entre esses intervalos de, no mínimo, Limiar amostras. Assim, podemos agora colher esses grãos numa estrutura (o uso de estrutura é vantajoso por várias razões, pois além de simplificar o código também nos permite futuramente associar mais atributos a cada grão):

for k=1:length(czLimiar)-1,

grao(k).sinal=s(czLimiar(k):czLimiar(k+1)-1);

end

Com esses grãos, podemos reconstituir quase perfeitamente o sinal original, a menos das bordas (sinal antes do primeiro cruzamento por zero e depois do último).

y=[];

for k=1:length(grao),

y=[y grao(k).sinal];

end

sound(y,fa)

Esticando o sinal no tempo (sem alterar seu conteúdo harmônico)

Como o limiar de 20 ms é muito maior que os períodos de ressonâncias da voz humana, já podemos usar os grão coletados diretamente para esticar o sinal simplesmente replicando os grão ao longo do tempo, por exemplo, para esticar o sinal para o dobro do seu tempo original, temos:

ydobrado=[];

for k=1:length(grao),

ydobrado=[ydobrado grao(k).sinal grao(k).sinal];

end

sound(ydobrado,fa)

Por esse raciocínio, podemos esticar o sinal num fator inteiro M apenas repetindo o grão M vezes. Por exemplo, para 5 vezes:

M=5;

yMvezes=[];

for k=1:length(grao),

for vez=1:M

yMvezes=[yMvezes grao(k).sinal ];

end

end

sound(yMvezes,fa)

Por outro lado, se o fator M for fracionário, para respeitarmos o princípio de que apenas grãos devem ser emendados uns nos outros (o que garante alguma fluidez no sinal), devemos procurar dentro de cada grão um novo cruzamento por zero que corresponda aproximadamente à fração da escala. Por exemplo, se M=1.7:

M=1.7;

EscalaInteira=floor(M);

EscalaFrac=M-floor(M);

y=[];

for k=1:length(grao),

for vez=1:EscalaInteira

y=[y grao(k).sinal ];

end

L=EscalaFrac*length(grao(k).sinal);

cz=find(grao(k).sinal(2:$)>=0 & grao(k).sinal(1:$-1)<0);

[val,pos]=min(abs(L-cz));

y=[y grao(k).sinal(1:cz(pos)-1)];

end

sound(y,fa)

Encolhendo o sinal no tempo (sem alterar seu conteúdo harmônico)

Para fatores de escala menores que 1 (e maiores que zero), usaremos o raciocínio já usado para fatores M fracionários, assim:

M=0.6;

y=[];

for k=1:length(grao),

L=M*length(grao(k).sinal);

cz=find(grao(k).sinal(2:$)>=0 & grao(k).sinal(1:$-1)<0);

[val,pos]=min(abs(L-cz));

y=[y grao(k).sinal(1:cz(pos)-1)];

end

sound(y,fa)

Alterando a frequência fundamental sem alterar o tempo ou o conteúdo harmônico (formantes)

O objetivo desta seção é mais delicado, pois é preciso

  1. estimar a frequência fundamental do segmento a ser processado (logo o segmento deve ser curto o suficiente para ter apenas uma frequência fundamental assumida estacionária)

  2. separar os grãos em subgrãos com um  limiar menor (subLimiar) dado pelo período correspondente (inverso da frequência fundamental estimada).

  3. aplicar a mudança de escala temporal a esses grão, tomando o cuidado de compensar a duração total do sinal (para que a alteração final ocorra apenas na frequência, não no tempo)

Para o passo 1, podemos tomar os grãos usados nas seções anteriores como candidatos a segmentos curtos o suficiente para ter apenas uma frequência fundamental assumida estacionária. O intervalo mínimo de 20 ms pode ser suficiente para isso, mas sugiro que os grãos sejam recoletados com um limiar um pouco maior, de 30 ms, para permitir a análise de vozes masculinas graves.

cz=find(s(2:$)>=0 & s(1:$-1)<0);

Limiar=round(0.03*fa);

clear czLimiar

czLimiar(1)=cz(1);

for k=2:length(cz),

if cz(k)-czLimiar($)>=Limiar,

czLimiar=[czLimiar cz(k)];

end,

end

clear grao

for k=1:length(czLimiar)-1,

grao(k).sinal=s(czLimiar(k):czLimiar(k+1)-1);

end

Agora podemos estimar a frequência fundamental (se houver) em cada grão. Para isso, qualquer método de estimação de freq. fundamental pode ser usado. Por simplicidade, aqui é usado um estimador básico, fundamentado na medida de autocorrelação do sinal,  implementado numa função para facilitar seu uso:

function [F0,relevancia]=estimaF0(x,fa)

Periodo_min=round(fa/500);

Periodo_max=round(fa/50);

for k= Periodo_min: Periodo_max

v1=x(1:$-k+1);

v1=v1-mean(v1);

v2=x(k:$);

v2=v2-mean(v2);

J(k)=sum(v1.*v2);

end

[relevancia,pos]=max(J);

relevancia=relevancia*length(x)/(length(x)-pos+1); // Correção da relevância

F0=fa/pos;

relevancia = relevancia/sum(x.^2);

endfunction

O parâmetro relevância deve ser usado para caracterizar o grão como harmônico ou não. Por exemplo, na figura seguinte temos o plot dos grãos 2 e 5

subplot(2,1,1), plot(grao(2).sinal)

subplot(2,1,2), plot(grao(5).sinal)

É evidente que o grão 5 possui uma simetria temporal (aparente repetição ao longo do tempo) que o grão 2 não possui. Isso é refletido nos valores obtidos nas chamadas à função estimaF0:

[F0,relevancia]=estimaF0(grao(2).sinal,fa)

relevancia =

0.2613474

F0 =

196.875

[F0,relevancia]=estimaF0(grao(5).sinal,fa)

relevancia =

0.7330093

F0 =

143.18182

Como a relevância é uma medida entre 0 e 1, o primeiro resultado, aprox. 0.26, indica que o F0 de ~197 Hz estimado não é confiável, e que provavelmente o grão 2 não é harmônico.

Já o segundo resultado, no entorno de 0.73, indica que o F0 de aprox. 143 Hz é confiável (note que esse é um valor comum de F0 para vozes masculina, como é o caso do sinal que escolhemos para ilustrar).

Tomemos portanto o grao 5 como exemplo do que deve acontecer com todos os grãos considerados harmônicos (os demais não devem ser processados). Devemos agora definir subgraos, que são grãos menores, com duração aproximada de um período dentro dos grãos escolhidos como harmônicos. Como o F0 do grão 5 é estimado em ~143.2 Hz, devemos impor um sub-limiar de aprox. fa/F0 para os subgrãos  do grão 5, e encontrá-los, como segue:

numero_grao=5;

LimiarSub=round(fa/F0);

cz=find(grao(numero_grao).sinal(2:$)>=0 & grao(numero_grao).sinal(1:$-1)<0);

clear czLimiar

czLimiar(1)=cz(1);

for k=2:length(cz),

if cz(k)-czLimiar($)>=LimiarSub,

czLimiar=[czLimiar cz(k)];

end,

end

clear subgrao

for k=1:length(czLimiar)-1,

subgrao(k).sinal=grao(numero_grao).sinal(czLimiar(k):czLimiar(k+1)-1);

end

Abaixando a frequência fundamental

Para baixar o F0 para, digamos, para 100 Hz, fazendo a voz soar grave, precisamos espaçar os inícios de grãos de round(LimiarSub *F0/100), ou um acréscimo de espaçamento de round(LimiarSub *(F0/100-1)), que é feito com a inclusão periódica de réplicas de um vetor com aprox. round(LimiarSub *(F0/100-1)) zeros.

acrescimo=zeros(1,round(LimiarSub *(F0/100-1)));

y=grao(numero_grao).sinal(1:czLimiar(1)-1); // Isso evita que o início do sinal seja perdido

for k=1:length(subgrao),

y=[y subgrao(k).sinal acrescimo];

end

y=[y grao(numero_grao).sinal(czLimiar($):$)]; // Evita a perda do segmento final do sinal

Embora o segmento de sinal seja muito curto já é possível escutar o efeito da alteração de F0:

sound(grao(numero_grao).sinal,fa)

sound(y,fa)

O efeito desejado foi obtido e pode ser replicado para todos os grãos harmônicos, mas ainda há um problema a ser considerado: o sinal y terminou ficando mais longo que o sinal original grao(numero_grao).sinal, por conta do acréscimo de amostras. Para compensar isso, é preciso cortar alguns grãos. Ou seja, se a diferença entre a duração do subgrão original e o subgrão estendido é de diferenca=LimiarSub *(F0/100-1), então se

length(subgrao)*(diferenca) > LimiarSub

então pelo menos um grão deve ser descartado para compensar o acréscimo de zeros (silêncio).

Mais precisamente, o número de subgrãos a serem descartados é

num_subgraos_descartados=floor(length(subgrao)*(diferenca)/LimiarSub)

e o código acima deve ser reescrito assim:

diferenca=LimiarSub *(F0/100-1);

num_subgraos_descartados=floor(length(subgrao)*(diferenca)/LimiarSub);

acrescimo=zeros(1,round(LimiarSub *(F0/100-1)));

y=grao(numero_grao).sinal(1:czLimiar(1)-1); // Isso evita que o início do sinal seja perdido

for k=1:length(subgrao)-num_subgraos_descartados,

y=[y subgrao(k).sinal acrescimo];

end

y=[y grao(numero_grao).sinal(czLimiar($):$)]; // Evita a perda do segmento final do sinal

Elevando a frequência fundamental

No sentido oposto ao da seção anterior, para elevar a frequência fundamental é preciso encurtar os subgrãos, ao invés de esticá-los. A exemplo do que foi feito com escalas fracionadas de tempo, isso só é possível se o subgrão possuir cruzamentos ascendentes por zero no seu interior.

Para elevar o F0 para, por exemplo, 220 Hz, fazendo a voz soar feminina, precisamos abreviar o espaçamento entre os inícios de grãos para round(LimiarSub *F0/220), e subgrãos devem ser acrescentados (repetidos), resultando no seguinte código

LimiarTruncado=round(fa/220);

diferenca=LimiarSub *(1-F0/220); // Atenção à diferença de sinal em relação ao código anterior

num_subgraos_acrescidos=floor(length(subgrao)*(diferenca)/LimiarSub);

y=grao(numero_grao).sinal(1:czLimiar(1)-1); // Isso evita que o início do sinal seja perdido

for k=1:length(subgrao),

czaux=find(subgrao(k).sinal(2:$)>=0 & subgrao(k).sinal(1:$-1)<0);

[val,pos]=min(abs(czaux-LimiarTruncado));

y=[y subgrao(k).sinal(1:czaux(pos))];

end

for k=1:length(subgrao),

[val,pos]=min(abs(czaux-LimiarTruncado));

y=[y subgrao(k).sinal(1:czaux(pos))];

end

y=[y grao(numero_grao).sinal(czLimiar($):$)]; // Evita a perda do segmento final do sinal

Para permitir uma percepção melhor desse efeito até mesmo para o grão escolhido, podemos esticar artificialmente o grão tanto no modo normal como com F0 alterado, assim:

// Versão F0 original

x=grao(numero_grao).sinal(1:czLimiar(1)-1);

for k=1:length(subgrao),

for e=1:10

x=[x subgrao(k).sinal];

end

end

x=[x grao(numero_grao).sinal(czLimiar($):$)];

sound(x,fa)

// Versão F0=220 Hz

LimiarTruncado=round(fa/220);

diferenca=LimiarSub *(1-F0/220); // Atenção à diferença de sinal em relação ao código anterior

num_subgraos_acrescidos=floor(length(subgrao)*(diferenca)/LimiarSub);

y=grao(numero_grao).sinal(1:czLimiar(1)-1); // Isso evita que o início do sinal seja perdido

for k=1:length(subgrao),

czaux=find(subgrao(k).sinal(2:$)>=0 & subgrao(k).sinal(1:$-1)<0);

[val,pos]=min(abs(czaux-LimiarTruncado));

for e=1:10

y=[y subgrao(k).sinal(1:czaux(pos))];

end

end

y=[y grao(numero_grao).sinal(czLimiar($):$)];

sound(y,fa)

(este documento será continuado…)

Sobre o Autor

Possui graduação em Engenharia Elétrica pela Universidade Federal da Paraíba (1992), mestrado em Engenharia Elétrica pela Universidade Estadual de Campinas (1995) e doutorado em “Automatique Et Traitement Du Signal” pela Université Paris-Sud 11 (2000). Atualmente é professor adjunto da Universidade Federal de Sergipe. Tem experiência na área de interface entre Ciência da Computação e Engenharia Elétrica, com ênfase em Processamento Digital de Sinais e Reconhecimento de Padrões, atuando principalmente nos seguintes temas: clustering, processamento de sinais dinâmicos e estimação de informação mútua aplicados à biometria e à televigilância médica.

Deixe uma resposta

O seu endereço de e-mail não será publicado.

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.