Perl Hackers Hub

第15回Perl meets beats―鳴らして学ぶシンセサイザー入門(3)

ビートの出力

(1)(2)までで単音の波形データが作れるようになりました。次は生成した波形データを任意のテンポとパターンで鳴らしてビートを刻みます。

発音タイミングの計算

ビートを刻むには、単音の波形データを間隔を置いて配置したチャンネルデータを作ります。間隔の計算には、BPMBeats Per Minute1分間に鳴る4分音符の回数)を決める必要があり、BPMが決まると配置先が決まります。この配置先のことを「発音タイミング」と呼び、図7の横軸の目盛りのことを指します。図7①は4分音符を配置するための間隔です。

図7 発音タイミング
図7 発音タイミング

Perlによる実装

リスト10は、ビートを刻むのに必要なパラメータの定義です。(1)はビートを刻むテンポで、BPMで定義します。(2)はビートをどうやって刻むかを定義した発音パターンで、1つの音色につき1つ定義します。定義した配列の中で「1」のときだけ鳴るように実装することで、任意のビートを刻むことができます。(3)は、発音パターンと音色と音量からなるチャンネルを定義しています。今回は、リスト6~8で4つの音色を定義したので、チャンネルを4つ用意しました。

リスト10 ビートのパラメータ
# (1)テンポ

my $bpm = 138; # beats per minute
# (2)発音パターンの定義
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 }, ┃(3)
    { 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 { # (1)
    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; # (2)
    my $seq_cnt = scalar( @{$seq} );
    my $interval = $samples_per_sec / $bps; # (3)
    my @plot_tmpl = ();
    for (my $i=0; $i<$seq_cnt; $i++) {
        push @plot_tmpl, int($i * $interval); # (4)
    }

    # 必要な配列サイズを計算して初期化
    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}; # (5)
    }

    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 ); # (1)
    my $vol = $ch_info->{vol};
    for (my $i=0; $i<scalar(@{$ch}); $i++) {
        $samples_beats[$i] += ( $ch->[$i] * $vol ); # (2)
    }
}

# クリップ
@samples_beats = map {
    ( 1.0 < $_ ) ? 1.0 : ( ($_ < -1.0) ? -1.0 : $_ ); # (3)
} @samples_beats;

WAVEファイルに出力

次に、生成した波形データをWAVEファイルに出力します。これでようやく、耳で聴くことができます。

WAVEファイルフォーマット

WAVEファイルは、RIFFResource Interchange File Formatチャンクをファイルに書き出したものです図8注1⁠。このチャンクChunkと呼ばれるデータ構造は、次のデータで構成されています。

  • 識別情報(ChunkID)
  • データ部のバイト数(ChunkSize)
  • データ部(ChunkID、ChunkSize以外)
図8 WAVEファイルフォーマット
図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;                        # (1)
    my $bits_per_sample = 16;                           # (2)
    my $file_name = shift;
    my $samples_ref = shift;

    my $block_size = $bits_per_sample / 8;              # (3)
    my $size = scalar(@{$samples_ref}) * $block_size;   # (4)
    my $bytes_per_sec = $block_size * $samples_per_sec; # (5)
    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) );  # (6)
    }
    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」です。お楽しみに。

おすすめ記事

記事・ニュース一覧