Ir para conteúdo

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

RSS iMasters

[Resolvido] Gerando sons dinâmicos através do ActionScript

Recommended Posts

Para iniciar, gostaria de chamar sua atenção para os requisitos necessários para o acompanhamento desse artigo. Para não se perder pelo conteúdo, você deve conhecer o método de trabalho do Flash, ActionScript Orientado a Objetos e uso de ByteArray. Esse é um artigo de nível intermediário/avançado.

Nosso objetivo aqui é gerar sons dinâmicos através do ActionScript, sem samples (mp3) na Library ou externos.

O conteúdo apresentado neste artigo é de um ex-aluno/amigo, André Anaya. O blog dele (blog.andreanaya.com) possui o mesmo conteúdo abaixo, com exceção aos arquivos fontes. O artigo é de ótimo nível e espero que todos gostem!

Passei as duas últimas semanas trabalhando num projeto envolvendo som. Infelizmente não posso mostrá-lo ainda, mas meu objetivo era tocar um monte de notas musicais em um intervalo especifico. A primeira coisa que eu tentei fazer foi carregar um monte de samples e toca-los quando precisasse, mas o desempenho ficou muito baixo com isso. Então minha solução foi procurar no Google e no lab do Andre Michelle para entender como trabalhar com sons dinâmicos.

Bom, ainda estou aprendendo, mas acho que muitas pessoas gostariam de trabalhar com isso e não têm a menor idéia por onde começar, por isso espero que esse post seja útil para essas pessoas.

A primeira coisa que precisamos entender é que o som é uma onda, e essa onde pode ter diferentes formatos dependendo do som que você está ouvindo.

41150.jpg

Agora imagine que essa onda é formada por pontos conectados. Cada ponto representa uma posição na onda em um determinado instante.

*mexa seu mouse horizontalmente para mudar o formato da onde

Certo, agora vamos chamar esses pontos de samples, e dizer que eles podem mudar seu valor de -1 a 1.

Basicamente é assim que o som funciona no Flash. Mas existem algumas coisas que devemos saber sobre a qualidade. Um som com qualidade boa depende do numero de samples tocados durante um segundo, e a precisão de cada ponto. O numero de samples tocados num segundo é chamado de sample rate, o Flash trabalha com um sample rate de 44100 samples por segundo.

Isso significa que cada segundo contém 44100 pontos. Cada sample, ou ponto, tem a precisão de 64 bits, sendo 32 bits para o canal esquerdo e 32 bits pro canal direito.

Agora vamos trazer isso para o Flash. Lembre-se que estamos usando o Player 10.

Primeiro nós criamos uma instancia de Sound, uma instancia de SoundChannel, adicionamos um listener ao som e tocamos ele no sound channel.

package {

import flash.display.Sprite;

import flash.events.SampleDataEvent;

import flash.media.Sound;

import flash.media.SoundChannel;

public class Main extends Sprite {

private var sound:Sound;

private var channel:SoundChannel;

public function Main():void{

sound = new Sound();

sound.addEventListener(SampleDataEvent.SAMPLE_DATA, writeData);

channel = sound.play();

}

private function writeData(e:SampleDataEvent):void{

}

}

}

 

O evento que sera ouvido é disparado antes do som ser executado e pede os dados de audio que compõe aquele fragmento de som. Você pode prover entre 2048 e 8192 samples. Quanto maior esse numero é, melhor é o desempenho, porém a latência é maior. Se você provê menos de 2048 samples (por chamada do listener de sampleData), a aplicação termina depois de tocar os samples restantes.

Agora vamor preencher os dados de audio com samples randomicos. Cada sample com um valor entre -1 e 1.

private function writeData(e:SampleDataEvent):void{

for(var i:Number = 0; i<8192; i++){

//Left Channel

e.data.writeFloat(Math.random()*2-1);

 

//RightChannel

e.data.writeFloat(Math.random()*2-1);

}

}

Não ouvimos nada além de um ruido. Para criar algum som decente precisamos criar um formato de onda que faça algum sentido. Existem alguns formatos básicos que podemos utilizar para criar algum som.

41151.jpg

Vamos utilizar uma senoide para criar uma nota musical A4. Se a frequência dessa nota é 4400.00 Hz, isso significa que nossa onda completa 440 ciclos por segundo.

41149.jpg

Essa é uma onda de 1Hz

E essa é uma onda de 2Hz

*Humanos só conseguem ouvir de 20 a 20,000 hertz

Se nos sabemos que cada segundo contém 44100 samples e uma nota A4 completa 440 ciclos por segundo, cada sample será um peço de 440/44100 de um ciclo. Um ciclo são 360 graus, ou Math.PI*2 radianos pois o Flash trabalha com radianos quando calcula senos. Convertendo isso para o Flash temos o seguinte:

private var phase:Number = 0;

private var step:Number = Math.PI*2 * 440/44100;

 

private function writeData(e:SampleDataEvent):void{

for(var i:Number = 0; i<8192; i++){

//Left Channel

e.data.writeFloat(Math.sin(phase));

 

//RightChannel

e.data.writeFloat(Math.sin(phase));

 

phase += step;

}

}

Agora conseguimos ouvir alguma coisa.

Veja o resultado aqui.

Se você quiser tocar notas diferentes você pode tentar essa fórmula onde a nota mais baixa é um A2.

phaseStep = 110.0*Math.pow( 2, octave + semiTone / 12 )/44100;

Vamos tentar criar um acorde maior A4.

Precisamos de 3 notas para isso, um A4, um C#4 e um E4.

private var A4:Number = 110.0*Math.pow( 2, 4 + 0 / 12 )/44100;private var Csharp4:Number = 110.0*Math.pow( 2, 4 + 4 / 12 )/44100;private var E4:Number = 110.0*Math.pow( 2, 4 + 7 / 12 )/44100;

E precisamos de uma fase para cada nota:

private var phaseA4:Number = 0;private var phaseCsharp4:Number = 0;private var phaseE4:Number = 0;

Depois adicionar os valores para compor o acorde

private function writeData(e:SampleDataEvent):void{

for(var i:Number = 0; i<8192; i++){

var amplitudeA4:Number = Math.sin(phaseA4);

var amplitudeCsharp4:Number = Math.sin(phaseCsharp4);

var amplitudeE4:Number = Math.sin(phaseE4);

 

phaseA4 += A4;

phaseCsharp4 += Csharp4;

phaseE4 += E4;

 

e.data.writeFloat(amplitudeA4+amplitudeCsharp4+amplitudeE4);

e.data.writeFloat(amplitudeA4+amplitudeCsharp4+amplitudeE4);

}

}

Quando precisamos unir dois ou mais sons, precisamos apenas somar cada valor para ter a amplitude final do sample, o problema é que quando escrevemos um valor maior que 1 ou menor que -1. Dessa forma distorcemos o som.

Veja isso.

Nesse caso podemos resolver o problema dividindo o valor final por 3, uma vez que temos 3 notas

private function writeData(e:SampleDataEvent):void{	for(var i:Number = 0; i<8192; i++){		var amplitudeA4:Number = Math.sin(phaseA4);		var amplitudeCsharp4:Number = Math.sin(phaseCsharp4);		var amplitudeE4:Number = Math.sin(phaseE4); 		phaseA4 += A4;		phaseCsharp4 += Csharp4;		phaseE4 += E4; 		e.data.writeFloat((amplitudeA4+amplitudeCsharp4+amplitudeE4)/3);		e.data.writeFloat((amplitudeA4+amplitudeCsharp4+amplitudeE4)/3);	}}

Agora ouvimos um som muito mais agradável.

Veja.

Baixe os arquivos aqui.

Nós podemos fazer muitas coisas com isso, mas podemos fazer muito mais usando samples pré-gravados e manipulando os samples para criar efeitos.

Agora, não vamos perder tempo criando o piano, então você pode baixar esse modelo.

Começando desse ponto, nós precisamos criar uma instância de som, uma instância de SoundChannel, adicionar o listeners e executar o som.

private var sound:Sound;private var channel:SoundChannel; public function Main():void{	init(); 	sound = new Sound();	sound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 	channel = sound.play();}private function onSampleData(e:SampleDataEvent):void{}

Agora nos vamos criar uma variável chamada BUFFER_SIZE que vai determinar o número de samples que serão tocados.

private var BUFFER_SIZE:uint = 3072;

Agora vamos criar um Vector que vai conter os dados de som.

private var buffer:Vector.<Vector.<Number>>;

Imagine um Vector como se fosse um Array. A diferença é que ele tem o tipo de dados que irá conter definido previamente. Nesse caso é um Vector bidimensional que trata números. A razão de utilizar um Vector em vez de um Array é que eles têm um processamento muito mais rápido e precisamos disso para obter uma latência baixa.

Agora iremos preencher esse Vector com valores que serão processados antes de escreve a informação de audio, para isso ele precisa ter o mesmo tamanho que o numero de samples que serão tocados.

private function createBuffer():void{	buffer = new Vector.<Vector.<Number>>(2, true);	buffer[0] = new Vector.<Number>(BUFFER_SIZE, true);	buffer[1] = new Vector.<Number>(BUFFER_SIZE, true); 	var i:uint; 	for(i = 0 ; i<BUFFER_SIZE; i++){		buffer[0][i] = 0.0;		buffer[1][i] = 0.0;	}}

O Vector é bidimensional pois temos o canal esquerdo e o direito.

Agora vamos utilizar esse buffer para escrever os dados de áudio.

private function onSampleData(e:SampleDataEvent):void{	var i:uint; 	for(i = 0; i<BUFFER_SIZE; i++){		e.data.writeFloat(buffer[0][i]);		e.data.writeFloat(buffer[1][i]);	}}

Até agora não conseguimos ouvir nada. Precisamos criar uma classe para as notas musicais. Essa classe vai conter os parâmetros da nota e vai processar a informação de áudio dessa nota.

Primeiro precisamos criar a base dessa nota.

package {	public class Note {		private var _phaseStep:Number;		private var _phase:Number; 		public function Note(semiTone:Number, octave:Number = 0):void{			_phaseStep = 440.0*Math.pow( 2, octave + semiTone / 12 )/44100;			_phase = 0;		}	}}

Agora vamos criar o método que vai processar a informação de áudio.

public function process(buffer:Vector.<Vector.<Number>>):void{	var l:Vector.<Number> = buffer[0];	var r:Vector.<Number> = buffer[1]; 	var i:uint;	var t:Number = l.length; 	var amplitude:Number; 	for(i = 0; i<t; i++){		amplitude = Math.sin(_phase);		_phase += _phaseStep; 		l[i] += amplitude;		r[i] += amplitude;	}}

O que esse método faz, é receber o buffer, guardar cada canal em uma variável e adicionar em cada sample o valor do seno baseado no phase da nota.

Para testar isso, vamos criar uma nota na classe principal.

private var note:Note = new Note(0, 2); // This is a A2

O som está estranho, pois precisamos zerar o buffer antes de adicionar novos dados.

private function clearBuffer():void{	var i:uint; 	for(i = 0 ; i<BUFFER_SIZE; i++){		buffer[0][i] = 0.0;		buffer[1][i] = 0.0;	}}

Agora precisamos chamar esse método antes de processar as notas:

private function onSampleData(e:SampleDataEvent):void{

clearBuffer();

 

var i:uint;

 

note.process(buffer);

 

for(i = 0; i<BUFFER_SIZE; i++){

e.data.writeFloat(buffer[0]);

e.data.writeFloat(buffer[1]);

}

}

OK, agora conseguimos ouvir alguma coisa. Entretando essa é uma nota musical então ela precisa soar e diminuir seu volume até terminar. Para fazer isso podemos pensar na amplitude da onda como um número que é multiplicado pelo volume da nota.

Se multiplicarmos a amplitude da nota por um número que diminui a cada sample, conseguimos diminuir o volume da nota para simular um piano.

De volta em nossa classe da nota, temos o seguinte:

package {

public class Note {

private var _phaseStep:Number;

private var _phase:Number;

 

private var _decay:uint = 30000;

 

public function Note(semiTone:Number, octave:Number = 0):void{

_phaseStep = 440.0*Math.pow( 2, octave + semiTone / 12 )/44100;

_phase = 0;

}

public function process(buffer:Vector.<Vector.<Number>>):void{

var l:Vector.<Number> = buffer[0];

var r:Vector.<Number> = buffer[1];

 

var i:uint;

var t:Number = l.length;

 

var amplitude:Number;

 

for(i = 0; i<t; i++){

amplitude = Math.sin(_phase)*_decay/30000;

_phase += _phaseStep;

 

l += amplitude;

r += amplitude;

 

if(--_decay<0){

_decay = 0;

}

}

}

}

}

O valor inicial da variável _decay é o número de samples que serão tocados até o fim da nota, nesse caso 30000. Isso significa que a cada sample esse valor vai diminuir até 0. E a razão de dividir essa variável pelo seu valor inicial é para conseguir a porcentagem de volume para cada sample.

Agora vamos criar um método em nossa classe Main que vai criar uma nota para cada tecla do piano. Primeiro vamos criar um Array para armazenar todas as notas.

private var notes:Array = new Array();

Agora o método createNote

private function createNote(semiTone:Number):void{	var note:Note = new Note(semiTone); 	notes.push(note);}

Então, quando apertamos uma tecla, esse método é chamado passando o semitom (semiTune) equivalente para aquela nota.

private function init():void{	stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown); 	C1.note = 0;	Csharp1.note = 1;	D1.note = 2;	Dsharp1.note = 3;	E1.note = 4;	F1.note = 5;	Fsharp1.note = 6;	G1.note = 7;	Gsharp1.note = 8;	A1.note = 9;	Asharp1.note = 10;	B1.note = 11;	C2.note = 12; 	C1.buttonMode = true;	Csharp1.buttonMode = true;	D1.buttonMode = true;	Dsharp1.buttonMode = true;	E1.buttonMode = true;	F1.buttonMode = true;	Fsharp1.buttonMode = true;	G1.buttonMode = true;	Gsharp1.buttonMode = true;	A1.buttonMode = true;	Asharp1.buttonMode = true;	B1.buttonMode = true;	C2.buttonMode = true; 	C1.addEventListener(MouseEvent.CLICK, onClick);	Csharp1.addEventListener(MouseEvent.CLICK, onClick);	D1.addEventListener(MouseEvent.CLICK, onClick);	Dsharp1.addEventListener(MouseEvent.CLICK, onClick);	E1.addEventListener(MouseEvent.CLICK, onClick);	F1.addEventListener(MouseEvent.CLICK, onClick);	Fsharp1.addEventListener(MouseEvent.CLICK, onClick);	G1.addEventListener(MouseEvent.CLICK, onClick);	Gsharp1.addEventListener(MouseEvent.CLICK, onClick);	A1.addEventListener(MouseEvent.CLICK, onClick);	Asharp1.addEventListener(MouseEvent.CLICK, onClick);	B1.addEventListener(MouseEvent.CLICK, onClick);	C2.addEventListener(MouseEvent.CLICK, onClick);}private function keyDown(e:KeyboardEvent):void{	if(e.keyCode == 65){		createNote(C1.note);	} else if(e.keyCode == 87){		createNote(Csharp1.note);	} else if(e.keyCode == 83){		createNote(D1.note);	} else if(e.keyCode == 69){		createNote(Dsharp1.note);	} else if(e.keyCode == 68){		createNote(E1.note);	} else if(e.keyCode == 70){		createNote(F1.note);	} else if(e.keyCode == 84){		createNote(Fsharp1.note);	} else if(e.keyCode == 71){		createNote(G1.note);	} else if(e.keyCode == 89){		createNote(Gsharp1.note);	} else if(e.keyCode == 72){		createNote(A1.note);	} else if(e.keyCode == 85){		createNote(Asharp1.note);	} else if(e.keyCode == 74){		createNote(B1.note);	} else if(e.keyCode == 75){		createNote(C2.note);	}}private function onClick(e:MouseEvent):void{	createNote(e.currentTarget.note);}

Agora precisamos processar essas notas antes de escrever a informação de áudio:

private function onSampleData(e:SampleDataEvent):void{	clearBuffer(); 	var i:uint; 	processNotes(); 	for(i = 0; i<BUFFER_SIZE; i++){		e.data.writeFloat(buffer[0][i]);		e.data.writeFloat(buffer[1][i]);	}}private function processNotes():void{	var i:uint;	var t:uint = notes.length; 	for(i = 0; i<t; i++){		notes[i].process(buffer);	}}

Parece que conseguimos mas, se você tocar muitas notas, sua aplicação vai começar a perder performance. Isso acontece porque toda vez que você toca uma nota, ela continua sendo processada mesmo que seu volume seja 0.

Para corrigir isso, precisamos retornar um boleano dizendo se aquela nota está muda ou não:

public function process(buffer:Vector.<Vector.<Number>>):Boolean{	var l:Vector.<Number> = buffer[0];	var r:Vector.<Number> = buffer[1]; 	var i:uint;	var t:Number = l.length; 	var amplitude:Number; 	for(i = 0; i<t; i++){		amplitude = Math.sin(_phase)*_decay/30000;		_phase += _phaseStep; 		l[i] += amplitude;		r[i] += amplitude; 		if(--_decay == 0){			return true;		}	}	return false;}

E com esse retorno nós podemos remover essa nota do array:

private function processNotes():void{	var i:uint = notes.length; 	while(--i>-1){		if(notes[i].process(buffer)){			notes.splice(i, 1);		}	}}

Aqui está o resultado final: 

Piano Source.

É um pouco confuso, mas se você tem alguma dúvida, sinta-se a vontade para perguntar.

Até a próxima!

 

http://imasters.com.br/artigo/22244/flash/gerando-sons-dinamicos-atraves-do-actionscript

Compartilhar este post


Link para o post
Compartilhar em outros sites

×

Informação importante

Ao usar o fórum, você concorda com nossos Termos e condições.