ビートの出力
(1) (2) までで単音の波形データが作れるようになりました。次は生成した波形データを任意のテンポとパターンで鳴らしてビートを刻みます。
発音タイミングの計算
ビートを刻むには、単音の波形データを間隔を置いて配置したチャンネルデータを作ります。間隔の計算には、BPM(Beats Per Minute 、1分間に鳴る4分音符の回数)を決める必要があり、BPMが決まると配置先が決まります。この配置先のことを「発音タイミング」と呼び、図7 の横軸の目盛りのことを指します。図7①は4分音符を配置するための間隔です。
図7 発音タイミング
Perlによる実装
リスト10 は、ビートを刻むのに必要なパラメータの定義です。(1) はビートを刻むテンポで、BPMで定義します。(2) はビートをどうやって刻むかを定義した発音パターンで、1つの音色につき1つ定義します。定義した配列の中で「1」のときだけ鳴るように実装することで、任意のビートを刻むことができます。(3) は、発音パターンと音色と音量からなるチャンネルを定義しています。今回は、リスト6~8で4つの音色を定義したので、チャンネルを4つ用意しました。
リスト10 ビートのパラメータ
# テンポ
my $bpm = 138; # beats per minute
# 発音パターンの定義
my $seq_kick = [ 1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0 ];
my $seq_snare = [ 0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0 ];
my $seq_o_hat = [ 0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0 ];
my $seq_c_hat = [ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ];
# 発音パターン, 音色, 音量の定義
my @beats = (
{ seq => $seq_kick, tone => $kick, vol => 1.00 },
{ seq => $seq_snare, tone => $snare, vol => 0.12 },
{ seq => $seq_o_hat, tone => $o_hat, vol => 0.06 },
{ seq => $seq_c_hat, tone => $c_hat, vol => 0.02 }
);
次に、これらのパラメータを使ってチャンネルデータを作ります(リスト11 ) 。(1) のcreate_channel()には、リスト10で定義したテンポとチャンネルを引数に与えます。今回は、発音パターンを16分音符として解釈したいので、(2) のようにBPMから1秒間に16分音符が鳴る回数を算出します。次にサンプリング周波数をこの回数で割って、16分音符の配置間隔である(3) を算出します。(4) はチャンネルデータである配列へ波形データを配置する際の格納開始添え字を算出しています。実際は、16分音符が鳴る間隔よりも単音の波形データのほうが長い場合も考慮して、単純にコピーして上書きしてしまうのではなく(5) のように足し込むことで、既存の波形データと同時に鳴らすことができます。
リスト11 チャンネルデータの生成
sub create_channel { #
my $samples_per_sec = 44100;
my $bpm = shift; # beats per minute
my $arg_ref = shift;
my $seq = $arg_ref->{seq};
my $tone = $arg_ref->{tone};
# 単音の波形データの生成
my $oneshot_ref = create_oneshot( $tone );
# 発音タイミングの計算
my $bps = ( $bpm / 60.0 ) * 4; #
my $seq_cnt = scalar( @{$seq} );
my $interval = $samples_per_sec / $bps; #
my @plot_tmpl = ();
for (my $i=0; $i<$seq_cnt; $i++) {
push @plot_tmpl, int($i * $interval); #
}
# 必要な配列サイズを計算して初期化
my $wav_size =
$plot_tmpl[$seq_cnt - 1] + scalar(@{$oneshot_ref});
my @channel = map { 0.0; } 1..$wav_size;
# チャンネルデータの生成
for (my $i=0; $i<$seq_cnt; $i++) {
if ( not $seq->[$i] ) { next; }
my $j = $plot_tmpl[$i];
map { $channel[$j++] += $_; } @{$oneshot_ref}; #
}
return \@channel;
}
# チャンネルデータの生成
my @channels = map {
create_channel( $bpm, $_ );
} @beats;
ミキシング
リスト11で生成したチャンネルデータを、それぞれ異なる音量でミックスする場合を考えます。
Perlによる実装
リスト12 は、生成したチャンネルデータをミックスしてクリップするコードです。クリップとは、任意の範囲を超える値を上限値または下限値で書き換える処理のことを指します。(1) はリスト11で生成したチャンネルデータです。チャンネルデータの音量を変えるには、(2) のようにすべてのデータに同じ係数を掛けることで実現できます。同時に音を鳴らすには、(2) のようにデータを足し込むことで実現できます。ただし、波形データをWAVEファイルに書き出す場合は、(2) の処理を行ったことで書き出し可能な範囲(-1.0~+1.0)を超えている場合が考えられるので、(3) のようにクリップが必要になります。
リスト12 ミキシングとクリップ処理
# ミックス
my @samples_beats = ();
foreach my $ch_info ( @beats ) {
my $ch = create_channel( $bpm, $ch_info ); #
my $vol = $ch_info->{vol};
for (my $i=0; $i<scalar(@{$ch}); $i++) {
$samples_beats[$i] += ( $ch->[$i] * $vol ); #
}
}
# クリップ
@samples_beats = map {
( 1.0 < $_ ) ? 1.0 : ( ($_ < -1.0) ? -1.0 : $_ ); #
} @samples_beats;
WAVEファイルに出力
次に、生成した波形データをWAVEファイルに出力します。これでようやく、耳で聴くことができます。
WAVEファイルフォーマット
WAVEファイルは、RIFF(Resource Interchange File Format )チャンクをファイルに書き出したものです(図8 ・注1 ) 。このチャンク(Chunk )と呼ばれるデータ構造は、次のデータで構成されています。
識別情報(ChunkID)
データ部のバイト数(ChunkSize)
データ部(ChunkID、ChunkSize以外)
図8 WAVEファイルフォーマット
図8のように、RIFFチャンクのデータ部には、FormType、fmt(フォーマット)チャンク、data(データ)チャンクが存在しています。ここで言う量子化ビット数とはファイル書き出し時におけるデータの表現方法を意味します。この値が16の場合は-1.0~+1.0
で扱ってきたデータを16ビットに収まる範囲、すなわち-32768~+32767
に変換して出力します。
Perlによる実装
リスト13 は、任意の波形データをWAVEファイルに出力するコードです。(1) はサンプリング周波数、(2) は量子化ビット数です。(3) は次に鳴るサンプルまでのバイト数なので、チャンネル数が1で、1サンプルあたり16ビットの場合は2になります。(4) は波形データのバイト数、(5) は音データの1秒間あたりのバイト数です。(6) は量子化ビット数が16ビットの場合における、-1.0~+1.0
に正規化された波形データのファイル出力処理です。量子化ビット数が8ビットの場合は、(6) で行う処理は異なります。
リスト13 WAVEファイル出力
sub save_as_wav {
my $samples_per_sec = 44100; #
my $bits_per_sample = 16; #
my $file_name = shift;
my $samples_ref = shift;
my $block_size = $bits_per_sample / 8; #
my $size = scalar(@{$samples_ref}) * $block_size; #
my $bytes_per_sec = $block_size * $samples_per_sec; #
my $header =
'RIFF' # ChunkID
. pack('L', ($size + 36)) # ChunkSize
. 'WAVE'; # FormType
my $fmt_chunk =
'fmt ' # ChunkID
. pack('L', 16) # ChunkSize
. pack('S', 1) # WaveFormatType
. pack('S', 1) # Channel
. pack('L', $samples_per_sec) # SamplesPerSec
. pack('L', $bytes_per_sec) # BytesPerSec
. pack('S', $block_size) # BlockSize
. pack('S', $bits_per_sample); # BitsPerSample
my $data_chunk =
'data' # ChunkID
. pack('L', $size); # ChunkSize
open( my $fh, '>', $file_name ) or die;
binmode $fh;
print $fh ($header . $fmt_chunk . $data_chunk);
foreach my $sample (@{$samples_ref}) {
print $fh pack( 's', int($sample * 32767.0) ); #
}
close $fh;
}
# これまでに生成した音データをファイルに書き出す
save_as_wav( 'sin.wav', \@sin_samples );
save_as_wav( 'sin_with_mod.wav', \@sin_with_mod_samples );
save_as_wav( 'kick.wav', $samples_kick_ref );
save_as_wav( 'snare.wav', $samples_snare_ref );
save_as_wav( 'o_hat.wav', $samples_o_hat_ref );
save_as_wav( 'c_hat.wav', $samples_h_hat_ref );
save_as_wav( 'samples_beats.wav', \@samples_beats );
まとめ
Perlでも音楽ができるということ、そして信号処理が楽しめるということがご理解いただけたでしょうか。組込み系の現場でも、このようにPerlを用いてテストデータの生成やデータ解析、変換を行っています。Webと縁遠い分野でもPerlが使われている例として参考になればと思います。
さて、次回の執筆者はText::Xslateでも有名なgfxこと藤吾郎さんで、テーマは「Perl Internal」です。お楽しみに。