É 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’ seguinte:
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
-
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)
-
separar os grãos em subgrãos com um limiar menor (subLimiar) dado pelo período correspondente (inverso da frequência fundamental estimada).
-
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)
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.
1 thought on “Manual tão simples quanto possível para deformação temporal e frequencial de sinais de fala”