前稿で波形表示すると言ってしまったので、ついでにスペアナも作ってみた。
一応出来たので頭の整理がてら書く。今回ではwaveのデータの読み込みと波形表示まで紹介する。
今回はProcessingを使ってみた。ProcessingはJavaベースなので(というかむしろJavaより有能だ)、特に詰まらずに完成した。
見た目はこんな感じ↓
・じゃけんデータ読み込みましょうね
ProcessingのloadBytes()
を使えばバイナリファイルをバイト配列として読み込める(超便利)。
で、前稿で説明したwaveファイル内の各データを読み込んでいくのだが、そもそもbyte[]から整数やら文字列やら浮動小数点数やらにしなければいけないうえに、これがWORD(16bit符号なし整数)だったりDWORD(32bit符号なし整数)だったり*1、挙句の果てに24bit符号あり整数(そんな型はどこにもない!)が出てきたりして面倒なのである。
というわけで、バイト配列から任意のバイト長の整数を錬成するメソッドを書いてしまえということになってできたのがこれ。
int ReadLittleEnd(int bytec,boolean signed){
byte[] subByte=new byte[bytec];
System.arraycopy(wavByte,bytePos,subByte,0,bytec);
bytePos+=bytec;
int v=0;
for(int i=0;i<bytec;i++){
int s=(subByte[i]&0xff)<<(i*8); //...(1)
v=v|s;
}
if(signed)return BitsToInt(v,bytec);
else return v;
}
int BitsToInt(int n,int bytec){
if(bytec>4)return n;
int mask=1<<(8*bytec-1);
if((n&mask)==mask){
n=-((~n&(mask-1))+1); //...(2)
}
return n;
}
何のことはない、ただ8ビットずつシフトして足し合わせているだけである...(1)。符号ありなら最上位ビットが1のとき2の補数を取って-1をかければよい...(2)。
これで24bit符号あり整数に煩わされずに済むようになったが、32bit符号なし整数がオーバーフローしてしまう可能性は残ってしまっている(unsigned intを作らなかったJavaが悪い)。
longにすればいいのだがそれも面倒で、そもそも実際にintの最大値を超える値が入ることなんかそうそうないし ー と考えてそのままにした(良い子はマネしてはいけない!)。
それと、フォーマットが32bit floatのときのためのfloatを返すのと、チャンクの識別子を読み取る用の文字列を返すのも用意しておく。
float ReadLittleEnd(){
byte[] subByte=new byte[4];
System.arraycopy(wavByte,bytePos,subByte,0,4);
bytePos+=4;
int vi=0;
for(int i=0;i<4;i++){
int s=(subByte[i]&0xff)<<(i*8);
vi=vi|s;
}
return Float.intBitsToFloat(vi);
}
String ReadId(){
String id="";
for(int i=0;i<4;i++){
char c=(char)wavByte[bytePos++];
id+=c;
}
return id;
}
続いてRIFFチャンク内の各チャンクのデータを読み取っていく。まずチャンク識別子とそのサイズを読み取り...(3)、fmtやdata以外の無用なチャンクがあった場合はそのサイズ分だけ参照する位置をスキップしている...(4)。
void Read(){
if(!ReadId().equals("RIFF")){
return;
}
int size=ReadLittleEnd(4,false);
println(String.format("RIFF Size:%d",size));
if(!ReadId().equals("WAVE")){
return;
}
while(bytePos<size-8){
String id=ReadId();
int chunkSize=ReadLittleEnd(4,false); //...(3)
println(String.format("id:%s/size:%d ",id,chunkSize));
if(id.equals("fmt ")){
if(chunkSize!=16){
return;
}
fmtCode=ReadLittleEnd(2,false);
ch=ReadLittleEnd(2,false);
sampleRate=ReadLittleEnd(4,false);
bytePerSec=ReadLittleEnd(4,false);
blockSize=ReadLittleEnd(2,false);
sampleByte=ReadLittleEnd(2,false)/8;
}else if(id.equals("data")){
sampleNum=chunkSize/(ch*sampleByte);
waveData=new float[ch][sampleNum];
if(fmtCode==1){
int maxLv=(1<<sampleByte*8-1)-1;
for(int i=0;i<sampleNum;i++){
for(int c=0;c<ch;c++){
int v=ReadLittleEnd(sampleByte,true);
waveData[c][i]=(float)v/maxLv; //...(5)
}
}
}else if(fmtCode==3){
for(int i=0;i<sampleNum;i++){
for(int c=0;c<ch;c++){
waveData[c][i]=ReadLittleEnd(); //...(6)
}
}
}
}else{
bytePos+=chunkSize; //...(4)
}
}
}
波形がintで記録されているときの振幅は最大値に対する割合(-1から1まで)だが...(5)、floatのときは振幅の値、-1から1までを直接格納している...(6)。
3.40e38まで表現できるのに1までしか使わないとはなんとも贅沢なものである。
・波形表示
ここからProcessingの出番になる。とはいえ各時刻の値を線で繋ぐだけなので特に難しいことはない。
リアルタイムで描画させたいのでProcessingのmillis()で経過時間を取得し、データのどこを参照するか決める。これは経過時間×サンプリングレートで出る...(7)。
マウスを押したら表示が一時停止する機能もつけた...(8)ので経過時間は変数timerに記録させている...(9)。
波形だけだとやや素っ気ないので、ついでに経過時間、参照しているサンプルの位置やフレームレートも表示させてみた...(10)。
void draw(){
int now=millis();
timer+=now-prevMillis; //...(9)
prevMillis=now;
float t=(float)timer/1000;
timePos=(int)(t*sampleRate); //...(7)
//
background(0);
fill(255);
textAlign(LEFT,TOP);
text(String.format("%7.3f sec. %9d/%d samples",
(float)timer/1000,timePos,sampleNum),5,5);
textAlign(RIGHT,TOP);
text(String.format("%dbit, %dHz %.1f FPS",
sampleByte*8,sampleRate,frameRate),width-5,5); //...(10)
//javaのstring.formatがわかりにくすぎる
if(sampleNum<timePos){
System.exit(0);
}else{
PlotWave();
}
}
void PlotWave(){
stroke(0,255,0);
translate(0,-height/(2*ch));
for(int c=0;c<ch;c++){
translate(0,height/ch);
float pPos=Float.NaN;
float pVal=Float.NaN;
for(int i=timePos;i<min(timePos*width,waveData[c].length);i++){
float val=waveData[c][i]*height/(2*ch);
float tPos=i-timePos;
if(Float.isNaN(pPos))
line(-1,0,tPos,val);
else
line(pPos,pVal,tPos,val);
pPos=tPos;
pVal=val;
}
}
translate(0,height/(2*ch));
translate(0,-height);
}
void mousePressed(){ //...(8)
if(running){
running=false;
noLoop();
}else{
running=true;
prevMillis=millis();
loop();
}
}
ここまでだとこんな感じになる。
---
今回はここまで。ソースコードだけはやたら長ったらしくて内容がスカスカだが勘弁してほしい。
次回はいよいよスペクトラムアナライザを実装していく。
Processingの何が便利ってコンソールに出力するときにいちいちSystem.outを書かなくていいとこじゃないですかね。