
Dziś pokażę w jaki sposób bez udziału zewnętrznych aplikacji, wyświetlić wykres fali dźwiękowej pliku WAV we Flash Playerze 9. W tym celu napisałem dwie klasy WaveSound oraz WaveExtractor. Pierwsza z nich jest wzorcem obiektu, który odzwierciedla plik WAV w Action Script 3.0. Druga natomiast to klasa statyczna, która tworzy obiekt WaveSound w oparciu o dane w postaci tablicy bajtów.
Klasy napisałem, opierając się na specyfikacji formatu WAV oraz źródłach biblioteki popforge, stworzonej przez Andre Michelle oraz Joa Ebert’a. Nie wykorzystałem gotowego rozwiązania, ponieważ wersja przedstawiona przez Andre i Joa jest wolniejsza od mojej o kilkadziesiąt procent, co jest bardzo ważne w przypadku przetwarzania tak dużej ilości danych.
W przypadku dźwięku stereofonicznego zapisanego w jakości CD jako PCM (Pulse Code Modulation), o próbkowaniu 44100 bitów na sekundę, dwu kanałach (dźwięk stereo), 60 sekundowy utwór zawiera 44100 * 60 sampli. Dla przykładowego pliku audio, którego długość wynosi około 4 minuty daje to ponad 10 000 000 sampli!
Pisząc sampel mam na myśli obiekt, który ma dwie właściwości: amplitudy kanału lewego oraz kanału prawego dźwięku w danym momencie czasu. Wartości każdego kanału to liczby rzeczywiste z zakresu [-1,1]. Dokładniej, są to wszystkie liczby, które można utworzyć, dzieląc liczby z przedziału od -32768 do 32767 (czyli liczby całkowite typu short – zapisane na dwóch bajtach) przez liczbę 32767: { -32768/ 32767 , -32767/ 32767, -32766/ 32767, … , 0/ 32767, 1/32767, 2/32767, … , 32767/ 32767 }.
Aby narysować wykres amplitudy od czasu dla pliku WAV wykonuję trzy kroki:
1) wczytuję plik WAV jako tablicę bajtów,
2) konwertuję ciąg bajtów na obiekt typu WaveSound przy pomocy metody extract() klasy WaveExtractor,
3) rysuję wykres w oparciu o tablicę sampli obiektu utworzonego w poprzednim kroku.
Poniżej zamieszczam kod źródłowy 4 klas: WaveFormDrawerDC, WaveExtractor, WaveSound oraz Output.
Klasę Output wykorzystuję wyłącznie jako zamiennik standardowej metody trace(), natomiast WaveFormDrawerDC, to Document Class tego przykładu. To tu znajduje się kod rysujący wykres i parametry konfiguracyjne.
WaveSound.as
package pl.rafalbociek.utils.waveform
{
/**
* Imports
*/
import flash.utils.ByteArray;
/**
* @author Rafal Bociek 2009
*
* <br><br>Klasa, bedaca odzwierciedleniem pliku wav
* po odkodowaniu z tablicy bajtow. <br>Przechowuje
* informacje o ilosci kanalow, predkosci
* probkowania,<br>tablice sampli dzwieku oraz kilka innych
* wlasciwosci.<br><br>
*
* Tablica sampli budowana jest w nastepujacy sposob:<br><br>
*
* 1) dla dzwieku stereofonicznego<br><br>
*
* <p style="padding-left:50px">samples = [l,r,l,r,l,r,l,r]</p><br>
*
* 2) dla dzwieku monofonicznego <br><br>
*
* <p style="padding-left:50px">samples = [l,l,l,l,l]</p><br>
*
* , gdzie l - wartosc amplitudy dzwieku kanalu lewego,<br>r - wartosc
* amplitudy dzwieku kanalu prawego. <br>Wartosci l,r sa liczbami
* rzeczywistymi z przedzialu [-1,-1].<br><br>
*
*/
public class WaveSound
{
public static const MONO : int = 1;
public static const STEREO : int = 2;
public static const BITS_8 : int = 8;
public static const BITS_16 : int = 16;
public static const CHUNK_FMT : String = 'fmt ';
public static const CHUNK_DATA : String = 'data';
private var __compression : uint;
private var __channels : uint;
private var __rate : uint;
private var __bps : uint;
private var __blockAlign : uint;
private var __bits : uint;
private var __samples : Array;
private var __samplesNum : uint;
private var __data : ByteArray;
private var __dataLen : uint;
static public function decode( bytes : ByteArray ) : WaveSound
{
return WaveExtractor.extract( bytes );
}
/**
* Konstruktor
*/
public function WaveSound ()
{
}
public function toString() : String
{
var str : String = '';
str += 'nnWaveSound parameters:nn';
str += 'COMPRESSION: tt' + __compression + 'n';
str += 'CHANNELS: ttt' + __channels + 'n';
str += 'RATE: tttt' + __rate + 'n';
str += 'BITS PER SECOND: t' + __bps + 'n';
str += 'SAMPLES NUMBER: t' + __samplesNum + 'n';
str += 'n';
return str;
}
/**
* Getter'y i setter'y
*/
public function get compression() : uint
{
return __compression;
}
public function set compression(_audioCompression : uint) : void
{
__compression = _audioCompression;
}
public function get channels() : uint
{
return __channels;
}
public function set channels(_audioChannels : uint) : void
{
__channels = _audioChannels;
}
public function get rate() : uint
{
return __rate;
}
public function set rate(_audioRate : uint) : void
{
__rate = _audioRate;
}
public function get bps() : uint
{
return __bps;
}
public function set bps(_audioBPS : uint) : void
{
__bps = _audioBPS;
}
public function get samples() : Array
{
return __samples;
}
public function set samples(_audioSamples : Array) : void
{
__samples = _audioSamples;
}
public function get samplesNum() : uint
{
return __samplesNum;
}
public function set samplesNum(_audioSamplesNum : uint) : void
{
__samplesNum = _audioSamplesNum;
}
public function get data() : ByteArray
{
return __data;
}
public function set data(_audioData : ByteArray) : void
{
__data = _audioData;
}
public function get bits() : uint
{
return __bits;
}
public function set bits(_audioBits : uint) : void
{
__bits = _audioBits;
}
public function get blockAlign() : uint
{
return __blockAlign;
}
public function set blockAlign(_audioBlockAlign : uint) : void
{
__blockAlign = _audioBlockAlign;
}
public function get dataLen () : uint
{
return __dataLen;
}
public function set dataLen (_audioDataLen : uint) : void
{
__dataLen = _audioDataLen;
}
}
}
WaveExtractor.as
package pl.rafalbociek.utils.waveform
{
/**
* Imports
*/
import flash.utils.ByteArray;
import flash.utils.Endian;
/**
*
* @author Rafal Bociek 2009<br><br>
*
* Klasa statyczna, sluzaca do dekodowania plikow wav i wyciagania z nich
* <br>tzw. sampli. Kazdy sampel posiada informacje o amplitudzie dzwieku
* w danym<br>momencie czasu dla kanalow lewego i prawego (dla dzwieku
* stereofonicznego)<br>oraz 'centralnego' dla sciezki monofonicznej.
*
* <br><br>Przyklad uzycia znajduje sie w klasie WaveFormDrawerDC.
*
*/
public class WaveExtractor
{
/**
* Funkcja dekoduje tablice bajtow podana w argumencie i zwraca<br>
* obiekt WaveSound.
*
* @param audioBA Tablica bajtow dzwieku wav.
* @return Obiekt typu WaveSound.
*/
static public function extract ( bytes : ByteArray ) : WaveSound
{
var audio : WaveSound = new WaveSound();
bytes.position = 0;
bytes.endian = Endian.LITTLE_ENDIAN;
bytes.readUTFBytes( 4 );
bytes.readUnsignedInt();
bytes.readUTFBytes( 4 );
var len : uint;
var pos : uint;
var chunkId : String;
var data : ByteArray = new ByteArray();
chunkId = bytes.readUTFBytes( 4 );
len = bytes.readUnsignedInt();
pos = bytes.position;
audio.compression = bytes.readUnsignedShort();
audio.channels = bytes.readUnsignedShort();
audio.rate = bytes.readUnsignedInt();
audio.bps = bytes.readUnsignedInt();
audio.blockAlign = bytes.readUnsignedShort();
audio.bits = bytes.readUnsignedShort();
while( bytes.position < bytes.length )
{
chunkId = bytes.readUTFBytes( 4 );
len = bytes.readUnsignedInt();
pos = bytes.position;
if( chunkId == WaveSound.CHUNK_DATA)
{
data = new ByteArray();
data.endian = Endian.LITTLE_ENDIAN;
data.writeBytes ( bytes , pos , len );
data.position = 0;
audio.data = data;
bytes.position = pos + len;
audio.dataLen = len;
}
else
{
bytes.position = pos + len;
}
}
audio.samplesNum = data.length;
if( audio.channels == WaveSound.STEREO )
{
audio.samplesNum = ( audio.samplesNum >> 1 );
}
if( audio.bits == WaveSound.BITS_16 )
{
audio.samplesNum = ( audio.samplesNum >> 1 );
}
audio.samples = $createSamplesArray ( audio );
return audio;
}
/**
* Funkcja odczytuje z obiektu WaveSound kolejne
* wartosci<br>sampli i zapisuje je w tablicy, ktora zwraca<br>
* po zakonczeniu dzialania.
*
* @param wav Obiekt typu WaveSound.
* @return Tablica sampli dzwieku podanego w argumencie.
*/
static internal function $createSamplesArray ( wav : WaveSound ) : Array
{
var data : ByteArray = wav.data;
var sampleCount : uint = wav.samplesNum;
var channels : uint = wav.channels;
var bits : uint = wav.bits;
var i : int = 0;
var samples : Array = new Array();
if( channels == WaveSound.STEREO )
{
if( bits == WaveSound.BITS_16 )
{
for( i = 0 ; i < sampleCount ; i++ )
{
samples.push ( data.readShort() / 0x7fff );
samples.push ( data.readShort() / 0x7fff );
}
}
else
{
for( i = 0 ; i < sampleCount ; i++ )
{
samples.push ( data.readUnsignedByte() / 0x80 - 1 );
samples.push ( data.readUnsignedByte() / 0x80 - 1 );
}
}
}
else if( channels == WaveSound.MONO )
{
if( bits == WaveSound.BITS_16 )
{
for( i = 0 ; i < sampleCount ; i++ )
{
samples.push ( data.readShort() / 0x7fff );
}
}
else
{
for( i = 0 ; i < sampleCount ; i++ )
{
samples.push ( data.readUnsignedByte() / 0x80 - 1 );
}
}
}
return samples;
}
}
}
WaveFormDrawerDC.as
package pl.rafalbociek.utils.waveform
{
/**
* Imports
*/
import pl.rafalbociek.utils.Output;
import flash.display.Graphics;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.utils.ByteArray;
/**
* @author Rafal Bociek 2009
*
* <br><br>Document Class aplikacji rysujacej wykres
* amplitudy dzwieku <br>w zaleznosci od czasu dla pliku audio
* zapisanego jako WAV.
*
*/
public class WaveFormDrawerDC extends Sprite
{
/**
* PARAMETRY WYKRESU / GRAPH PARAMETERS
*
*
* G_W - szerokosc wykresu (GRAPH WIDTH)
* G_H - wysokosc wykresu (GRAPH HEIGHT)
*
* G_LT - grubosc linii wykresu (GRAPH LINE THICKNESS)
* G_LC - kolor linii wykresu (GRAPH LINE COLOR)
* G_LA - przezroczystosc linii wykresu (GRAPH LINE ALPHA)
*
* GD_LT - grubosc linii siatki (GRID LINE THICKNESS)
* GD_LC - kolor linii siatki (GRID LINE COLOR)
* GD_LA - przezroczystosc linii siatki (GRID LINE ALPHA)
* GD_BGC - kolor tla siatki (GRID BACKGROUND COLOR)
* GD_BGA - przezroczystosc tla siatki (GRID BACKGROUND ALPHA)
*
* G_CC - kolor punktu na wykresie (GRAPH CIRCLE COLOR)
* G_CA - przezroczystosc punktu na wykresie (GRAPH CIRCLE ALPHA)
*
* GD_XD - liczba linii pionowych siatki (GRID X-AXIS DIVISION)
* GD_YD - liczba linii poziomych siatki (GRID Y-AXIS DIVISION)
*
* F_URL - adres URL do pliku .wav (WAVE FILE URL)
*
*/
public static const G_W : int = 600;
public static const G_H : int = 80;
public static const G_LT : int = 1;
public static const G_LC : int = 0x9c9c9c;
public static const G_LA : Number = 1.0;
public static const GD_LT : int = 1;
public static const GD_LC : uint = 0xf0f1f6;
public static const GD_LA : Number = 1.0;
public static const GD_BGC : uint = 0xfdfdfd;
public static const GD_BGA : Number = 1.0;
public static const G_CC : int = 0xcc0033;
public static const G_CA : Number = 1.0;
public static const GD_XD : int = 300; // 5*60
public static const GD_YD : int = 80; // 5*16;
public static const F_URL : String = 'embed/sampleAudio.wav';
/**
* Wlasciwosci prywatne
*/
private var __waveSound : WaveSound = null;
/**
* Konstruktor
*/
public function WaveFormDrawerDC()
{
/**
* Przy pomocy URLLoader'a wczytuje plik wav jako tablice bajtow.
*/
Output.debugEnabled = true;
var uL : URLLoader = new URLLoader();
uL.dataFormat = URLLoaderDataFormat.BINARY;
uL.addEventListener(Event.COMPLETE , __completeEH);
uL.addEventListener(IOErrorEvent.IO_ERROR , __ioeEH);
uL.addEventListener(SecurityErrorEvent.SECURITY_ERROR , __seEH);
uL.load( new URLRequest( F_URL ) );
}
/**
* Event handler wywolywany w przypadku
* wystapienia bledu bezpieczenstwa
* przy zadaniu wczytania pliku.
*/
private function __seEH (event : SecurityErrorEvent) : void
{
Output.println('Wystapil blad bezpieczenstwa!');
}
/**
* Event handler wywolywany w przypadku
* wystapienia bledu wejscia/wyjscia.
*/
private function __ioeEH (event : IOErrorEvent) : void
{
Output.println('Wystapil blad wejscia!');
}
/**
* Event handler wywolywany po zakonczeniu operacji wczytania pliku.
*/
private function __completeEH(event : Event) : void
{
Output.println('Plik wczytany poprawnie.');
__drawGraph((event.target as URLLoader).data);
}
/**
* Metoda rysujaca wykres i dodajaca go na scene.
*/
private function __drawGraph(byteArray : ByteArray) : void
{
/**
* Dekoduje tablice bajtow do AudioWaveFormat.
*/
var start : Number = (new Date()).getTime();
__waveSound = WaveExtractor.extract(byteArray);
Output.println(__waveSound.toString());
/**
* Tworze referencje do tablicy sampli wczytanego pliku.
*/
var sRef : Array = __waveSound.samples;
/**
* Tworze dwa kontenery typu Sprite dla kanalow lewego i prawego
* oraz definiuje referencje do ich parametrow graphics.
*/
var leftChannel : Sprite = new Sprite();
var rightChannel : Sprite = new Sprite();
var lGfx : Graphics = leftChannel.graphics;
var rGfx : Graphics = rightChannel.graphics;
/**
* Rysuje siatke wykresu.
*/
__drawGrid( lGfx , G_W , G_H * 2 , GD_XD , GD_YD );
__drawGrid( rGfx , G_W , G_H * 2 , GD_XD , GD_YD );
/**
* Ustawiam lewy i prawy zakres czasu, dla ktorego kresle wykres.
*/
var left : uint = 2 * __waveSound.rate * 2;
var right : uint = 2 * __waveSound.rate * 3;
/**
* Definiuje zmienne potrzebne przy rysowaniu.
*/
var i : int = 0;
var step : int = 0;
var range : int = right - left;
/**
* W zaleznosci od ilosci kanalow utworu
* (STEREO lub MONO) rysuje wykres fali dzwiekowej.
*/
if(__waveSound.channels == WaveSound.STEREO)
{
/**
* Obliczam krok iteracji.
*
* Jezeli ilosc sampli, dla ktorych rysuje wykres jest mniejsza
* od (GRAPH_WIDTH * 2 * 100), to krok = 2. W innym wypadku
* krok = 2 * Math.ceil(range / GRAPH_WIDTH / 2 / 100).
*/
step = (range <= G_W * 2 * 100)? 2 : 2 * Math.ceil(range/G_W/2/100);
/**
* Kresle wykres
*/
lGfx.moveTo(0 , G_H * sRef[0]);
rGfx.moveTo(0 , G_H * sRef[1]);
lGfx.lineStyle(0,0,0);
rGfx.lineStyle(0,0,0);
/**
* Wykres dla fragmentu utworu o ilosci sampli mniejszej od
* 2 * G_W rysuje z wieksza dokladnoscia - stad ponizszy warunek.
*/
if(range <= G_W * 2)
{
for ( i = left + 1 ; i <= right ; i += step)
{
lGfx.lineStyle( G_LT , G_LC , G_LA );
rGfx.lineStyle( G_LT , G_LC , G_LA );
lGfx.lineTo((i - left) / range * G_W , - G_H * sRef[i-1]);
rGfx.lineTo((i - left) / range * G_W , - G_H * sRef[i]);
lGfx.lineStyle(0,0,0);
rGfx.lineStyle(0,0,0);
lGfx.beginFill(G_CC , G_CA);
rGfx.beginFill(G_CC , G_CA);
lGfx.drawCircle(
(i - left) / range * G_W,
- G_H * sRef[i-1],
G_LT*2
);
rGfx.drawCircle(
(i - left) / range * G_W,
- G_H * sRef[i],
G_LT*2
);
lGfx.endFill();
rGfx.endFill();
}
lGfx.beginFill(G_CC , G_CA);
rGfx.beginFill(G_CC , G_CA);
lGfx.drawCircle(0 - G_LT , G_H * sRef[0] - 1,G_LT * 2 );
rGfx.drawCircle(0 - G_LT , G_H * sRef[1] - 1,G_LT * 2 );
lGfx.endFill();
rGfx.endFill();
}
else
{
lGfx.endFill();
rGfx.endFill();
lGfx.lineStyle( G_LT , G_CC , G_LA );
rGfx.lineStyle( G_LT , G_CC , G_LA );
for ( i = left + 1; i <= right ; i += step)
{
lGfx.lineTo(
(i - left) / range * G_W ,
- G_H * sRef[i-1]
);
rGfx.lineTo(
(i - left) / range * G_W ,
- G_H * sRef[i]
);
}
}
}
else if(__waveSound.channels == WaveSound.MONO)
{
/**
* Wersja dla dzwieku MONO.
*/
step = (range <= G_W * 1 * 50) ? 1 : range / G_W / 100;
lGfx.moveTo(0 , G_W * sRef[0]);
rGfx.moveTo(0 , G_W * sRef[0]);
for ( i = left + 1 ; i <= right ; i += step)
{
lGfx.lineTo (
(i - left) / range * G_W ,
G_W * -1 / sRef[i]
);
rGfx.lineTo (
(i - left) / range * G_W ,
G_W * -1 / sRef[i]
);
}
}
/**
* Pozycjonuje i dodaje na display liste
* wykresy kanalow lewego i prawego.
*/
leftChannel.x = 20;
rightChannel.x = 20;
leftChannel.y = 100;
rightChannel.y = 300;
addChild( leftChannel );
addChild( rightChannel );
/**
* Wyswietlam czas trwania operacji
*/
Output.println('Duration: ' + ((new Date()).getTime() - start));
}
/**
* Metoda rysujaca siatke pod wykres fali dzwiekowej.<br>
* @param gfx Referencja do obiektu typu Graphics,
* na ktorym powstanie siatka.
* @param width Szerokosc wykresu.
* @param height Wysokosc wykresu * 2.
* @param xSteps Ilosc krokow podzialu osi OX.
* @param ySteps Ilosc krokow podzialu osi OY.
*/
private function __drawGrid (
gfx : Graphics ,
width : int = 600,
height : int = 80,
xSteps : int = 0,
ySteps : int = 10
) : void
{
gfx.beginFill( GD_BGC , GD_BGA);
gfx.drawRect(0, -height/2, width, height);
gfx.endFill();
var i : int;
gfx.lineStyle( GD_LT , GD_LC , GD_LA );
for (i = 0 ; i <= ySteps ; i++)
{
gfx.moveTo(0 , i * height / ySteps - height/2);
gfx.lineTo(width , i * height / ySteps - height/2);
}
for (i = 0 ; i <= xSteps ; i++)
{
gfx.moveTo(i * width / xSteps , - height/2);
gfx.lineTo(i * width / xSteps , height/2);
}
gfx.lineStyle( GD_LT , GD_LC - 10000 , GD_LA );
gfx.moveTo(0 , 0);
gfx.lineTo(width , 0);
}
}
}
Output.as
package pl.rafalbociek.utils
{
/**
* Imports
*/
import flash.external.ExternalInterface;
/**
* @author Rafal Bociek 2009
*
* <br><br>Klasa zastepujaca standardowa metode trace().
* <br>Dzieki niej mozliwe jest proste debug'owanie w konsoli FireBug'a.
*
*/
public class Output
{
/**
* Flaga, oznaczajaca czy funkcja debugowania jest aktywna.
*/
private static var __debugEnabled : Boolean;
/**
* Getter'y i setter'y
*/
public static function set debugEnabled(value : Boolean) : void
{
__debugEnabled = value;
}
public static function get debugEnabled() : Boolean
{
return __debugEnabled;
}
/**
* Metoda wyswietla w oknie Output oraz w konsoli<br>Firebug'a
* kolejne argumenty funkcji.
* @param args Lista wartosci do wyrzucenia na konsole.
*/
public static function print(...args) : void
{
if(__debugEnabled)
{
for (var i : int = 0; i < args.length; i++)
{
trace(args[i]);
if(ExternalInterface.available)
{
ExternalInterface.call('console.log' , args[i]);
}
}
}
}
/**
* Metoda wyswietla w oknie Output oraz w konsoli<br>Firebug'a
* elementy tablicy.
* @param array Tablica do wyrzucenia na konsole.
*/
public static function printArray(array : Array) : void
{
if(__debugEnabled)
{
var str : String;
if(array.length > 0)
{
for (var i : int = 0; i < array.length; i++)
{
str = 'INDEX: ' + i + ' | ' + 'VALUE: ' + array[i];
trace(str);
if(ExternalInterface.available)
{
ExternalInterface.call( 'console.log' , str );
}
}
}
else
{
for (var key : String in array)
{
str = 'KEY: ' + key + ' | ' + 'VALUE: ' + array[key];
trace(str);
if(ExternalInterface.available)
{
ExternalInterface.call( 'console.log' , str );
}
}
}
}
}
/**
* Metoda wyswietla ciag znakow str w oknie Output oraz konsoli<br>
* Firebug'a.
* @param str Ciag znakow do wyswietlenia.
*/
public static function println(str : String) : void
{
if(__debugEnabled)
{
trace(str);
if(ExternalInterface.available)
{
ExternalInterface.call('console.log', str);
}
}
}
}
}
Przed uruchomieniem trzeba zmienić ścieżkę do pliku w klasie WaveFormDrawerDC w linijce public static const F_URL : String = ‘embed/sampleAudio.wav’; tak by wskazywał plik WAV, którego wykres będzie rysowany. Ze względu na wielkość plików WAV i praw do nich, nie zamieszczam ich na serwerze.
Kody źródłowe oraz pliki fla i swf do pobrania stąd.