Dynamic Sound – Part 2

No ultimo post eu comecei a falar sobre som dinâmico no flash. Agora nos vamos fazer algo mais prático com o que vimos. Vamos criar um piano que toca notas clicando ou apertando as teclas.

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

This movie requires Flash Player 9

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 Vectoro como se fosse um Array, a diferença é que ele tem o tipo de dados que ira conter definido préviamente. Nesse caso é um Vector bidimensional que trata números. A razão de utilizar um Vector em vez de um Array, é que eles tem 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 audio.

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 parametros da nota e vai processar a informação de audio 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 audio.

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 testa isso, vamos criar uma nota na classe principal.

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

e chamar o método process da nota antes de escrever a informação de audio no onSampleData

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

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][i]);
		e.data.writeFloat(buffer[1][i]);
	}
}

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. Então se multiplicarmos a amplitude da note 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[i] += amplitude;
				r[i] += amplitude;
 
				if(--_decay<0){
					_decay = 0;
				}
			}
		}
	}
}

O valor inicial da variavel _decay é o número de sample 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 nós apertamos uma tecla, esse método é chamado passando o semi tom (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 audio

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);
		}
	}
}

This movie requires Flash Player 9

É um pouco confuso, mas se você tem alguma dúvida, sinta-se a vontade para perguntar. Na próxima vez vamos trabalhar audio extraido de arquivos externos.

Source

Tags: , , , ,

7 Responses to “Dynamic Sound – Part 2”

  1. [...] This post was Twitted by scarpelini – Real-url.org [...]

  2. bechar says:

    great stuff…

  3. jonah says:

    Is the delay between click and a note playing just standard as3 latency? No way around it?

  4. Anaya says:

    The delay depends on the number of samples you provide. The minimum number of samples is 2048, this results in a delay of 46ms. This is because the SampleDataEvent requests the audio data before play it, so if you hit a note, it will be played after the previous audio data that was writen.

    However, you have to be careful with the number of samples that you provide, because less samples means more data being processed in a small period of time.

  5. Amazing Anaya… very good sample.
    []’s

  6. machian says:

    Great Tutorial and nice explanations!

    one completely different question:
    do you think with SampleDataEvent one can make a CLEAN fade in/out or crossfade for mp3 files? I tried several ways with timers and OnEnterFrame and even Tween Engines- but it always makes little click noises for every step.. or do you know a better way?
    I’m trying to create a class that i could use for multi-sample(?) web-sounddesign for my diploma…

    anyway great work!
    Marian

  7. psybermoon says:

    Obrigado, very nice job!!

Leave a Reply