Perl Hackers Hub

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

この記事を読むのに必要な時間:およそ 2 分

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回は@techno_nekoこと伊藤智章さんが,Perlで音を扱う方法について紹介します。

Perlで「音」は扱える?

Perlと言えば文字列操作やWebアプリケーションのイメージが強いと思いますが,今回は「音」を扱って信号処理を行うという,ちょっと特殊なPerlの使い方を紹介します。まずPerlで音を作って,次にビートを刻み,最後にWAVEファイルに出力する方法を説明します。

なお,本稿のコードはWEB+DB PRESS Vol.69のサポートサイトからダウンロードできますので,ぜひみなさんも実際に動かして音を聴いてみてください。

波形のお話

普段みなさんが耳にしている音は,とても複雑な波形(空気の振動)です。今回はその複雑な波形を作るために,いくつかの基本波形を合成して音を作ります図1)⁠これらの波形は,三角関数のように同じデータを繰り返すものと,乱数を用いて生成したノイズで表現されます。

図1 波形の種類

図1 波形の種類

リスト1は波形を生成する関数を返すコードです。最初に,ノイズ波形のもととなるノイズデータの初期化を行います。ノイズ波形とは,rand()の戻り値のように周期を持たない波形を指します。ノイズデータは,好みの音が出る状態を保持するために,(1)のようにsrand()による初期化を行ってあらかじめ配列に格納しておきます。これにより,今回の音作りに適したノイズ波形を安定的に生成できます。実際に波形データを生成する関数は(2)のように定義して,あとのコードでこれらを簡単に呼び出せるように,波形の名前を指定して関数を取得できる(3)のcreate_mod_func()を定義しています。

リスト1 波形を生成する関数

use strict;
use warnings;
use Math::Trig qw( pi );

srand( 2 ); # (1)好みの音が得られるように初期化
my @noise = map { rand( 2.0 ) - 1.0; } 1..1024;

my %func_table = ( # (2)
    'pulse' => sub { # 矩形波
        return ( $_[0] < 0.5 ) ? -1.0 : 1.0;
    },
    'sin' => sub { # サイン波(正弦波)
        return sin( 2.0 * pi() * $_[0] );
    },
    'saw' => sub { # のこぎり波
        return ( 2.0 * $_[0] ) - 1.0;
    },
    'tri' => sub { # 三角波
        if ( $_[0] < 0.5 ) {
            # -1.0 -> +1.0
            return -1.0 + ( 4.0 * $_[0] );
        }
        else {
            # +1.0 -> -1.0
            return 1.0 - ( 4.0 * ($_[0] - 0.5) );
        }
    },
    'noise' => sub { # ノイズ
        if ( $_[0] < 1.0 ) {
            my $idx = int( $_[0] * scalar(@noise) );
            return $noise[$idx];
        }
        else {
            return 0.0;
        }
    }
);

sub create_mod_func { # (3)
    my $func = $func_table{$_[0]} or die;
    return $func;
}

音程のお話

図2のように,1オクターブを12等分して算出した音律(周波数と音程の関係)を十二平均律と言います。今回は,一般的に多く採用されている基準となるラの音を440Hzとした場合の,残りの音程の周波数を算出する方法を紹介します。

図2 音程と周波数

図2 音程と周波数

Perlによる実装

リスト2は,図2のindexと周波数の関係をコードに落とし込んだものです。たとえばドの音の周波数を計算したい場合は,図2の横軸からドに対応するindexを選択して,(1)のように関数の引数として与えて算出します。さらに1オクターブ高い音の周波数を計算する場合は(2)のように12足したものを与え,逆に1オクターブ低い音の周波数を計算する場合は(3)のように12引いたものを引数に与えます。

リスト2 indexから周波数を求める

sub index_to_freq {
    my $index = shift;
    return 440.0 * ( 2.0 ** ($index / 12.0) );
}

# ドの音の周波数を計算する
my $freq_C2 = index_to_freq( 3 );      # (1)
my $freq_C3 = index_to_freq( 3 + 12 ); # (2)
my $freq_C4 = index_to_freq( 3 - 12 ); # (3)

Column クロージャ

以降の本文でクロージャを使ったコードが頻繁に出てくるので,ここで説明しておきます。

リストaは,任意の周期で戻り値が変化する関数を生成する例です。(1)の関数で引数から閾値(いきち)とカウンタの変化量を設定して,カウンタ変数とフラグを初期化した関数を生成します。生成した関数は,呼び出すたびに(2)でカウンタ変数を更新して,(3)で閾値を超えている場合は戻り値に設定する値を更新しています。(1)で生成される関数のように,その関数が定義されている環境(my で宣言されている閾値や変化量など)を参照する関数のことをクロージャと呼びます。特に本稿で頻出するcreate_modulator()は,その代表例です。

リストa 任意の周期で戻り値が変化する関数を作る

use strict;

my $f1 = create_func( 4, 1 );
my $f2 = create_func( 4, 2 );
                                          # f1, f2
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  0,  0
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  0,  0
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  0,  1
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  0,  1
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  1,  0
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  1,  0
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  1,  1
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  1,  1
printf( "%2d, %2d\n", $f1->(), $f2->() ); #  0,  0

sub create_func {                      # (1)
    my ( $threshold, $step ) = @_;
    my $counter = 0;
    my $flag = 0;
    return sub {
        $counter += $step;             # (2)
        if ( $threshold < $counter ) { # (3)
            $flag = ( $flag ) ? 0 : 1;
            $counter -= $threshold;
        }
        return $flag;
    };
}

著者プロフィール

伊藤智章(いとうともあき)

組み込み系の会社に所属し,ファームウェアとGUIアプリケーションの開発を担当。その開発作業の効率化のためにPerlを使用している。

主に「Hokkaido.pm」に出没して,あご髭が長いというだけで北海道のPerl(?)仙人と呼ばれている。

Twitter:techno_neko

写真:Japan Perl Association

コメント

コメントの記入