例題2-1:wavファイルを作る

B: うーむ。ぶつぶつ。

A: お、論文を読んでるな。感心感心。

B: あ、ちょうどいいところに。ちょっとご相談があるんですが。この論文で2000Hzと2002Hzの音を重ねて鳴らすって刺激が出てくるんですけど、どんなふうに聞こえるのか全然イメージ出来ないんです。

A: 聴覚かあ。聴覚は私もさっぱりわからんなあ。

B: こういう刺激ってPCで簡単に出来ないんですか。

A: pygameの機能を使ったら簡単にできるだろうな。やったことないけど。

B:虎屋萬寿堂のタイ焼き でどうです。

A: むむっ、あの 伝説のタイ焼き とな。よっしゃ、ちょっと待ってろ。

A: ふむ。wavを経由するのは泥臭いがこんなもんかな。おーい、出来たぞー。

B: はーい。案外時間かかりましたねー。

A: 音関連のプログラムはあまり書いた事ないからな。今回のプログラムは音を作る部分と再生する部分の二つに分かれているぞ。まずは音を作る部分。

  • 行番号なしのソースファイルをダウンロード→ 02-1.py

 1#!/usr/bin/env python
 2# -*- coding:shift_jis -*-
 3import wave
 4from numpy import *
 5
 6freq = 44100
 7t = linspace(0.0,1.0,freq)
 8amp = 15000
 9
10#モノラル出力2000Hzと2002Hzの合成音
11w = amp*(sin(2000*2*pi*t)+sin(2002*2*pi*t))/2
12data = w.astype(int16)
13out = wave.open('mono.wav','w')
14out.setnchannels(1) #mono
15out.setsampwidth(2) #16bits
16out.setframerate(freq)
17out.writeframes(data.tostring())
18out.close()
19
20#ステレオ出力:左に2000Hz、右に2002Hz
21wL = amp*sin(2000*2*pi*t)
22wR = amp*sin(2002*2*pi*t)
23w = array([wL,wR]).transpose()
24data = w.astype(int16)
25out = wave.open('stereo.wav','w')
26out.setnchannels(2) #stereo
27out.setsampwidth(2) #16bits
28out.setframerate(freq)
29out.writeframes(data.tostring())
30out.close()
31
32

B: このプログラムが短く感じられるのは成長した証ですかね。

A: かもな。今回はモノラルの音とステレオの音を作ってみたんだけど、モノラルだけで良ければ18行目で終わりだ。

B: 案外簡単なんですね。

A: ぱぱっと解説するぞ。まず3行目と4行目。waveモジュールとnumpyパッケージをインポートしている。この二つがカギだ。waveは wav形式の音データファイルを読み書きする機能を、numpyはpythonに強力な行列演算機能を追加する。

B: 行列? 「行列ができるなんとやら」の行列じゃないですよね?

A: B君は高校数学でこんなのを習わなかったかな?

../_images/02-1-01.png

B:

A: おーい。戻ってこーい。

B: こんなの知らないです…。なんですかこれ。

A: これが行列って言うのさ。知ってるとすんごい便利なんだけどな。まあ今は数学の授業じゃないんで、また機会があれば。 ところでB君はアナログとデジタルの違いってわかるかい?

B: アナログ…はなんか丸くってデジタルはカクカクした感じ。

A: なんだそりゃ。でも気持ちはわからんでもない。すごくおおざっぱに言うと、何かの量を表す時に、飛び飛びの数値で表したものがデジタル、連続的な数値で表したものがアナログだ。 滑らかに変化するものをデジタルで表現しようとするとカクカクになるから、デジタル=カクカクと言えなくもない。 それはさておき、音というのは空気の振動なわけだが、空気の振動はアナログな量だ。しかし、コンピュータでそれを表現しようとすると、デジタルな量に変換しなければならない。 イメージとしてはこんな感じだな。本来の音は赤線のように振動していて、それを青い塗りつぶしのように変換しないといけない。

../_images/02-1-02.png

B: ははぁ。わかるようなわからないような。

A: B君が読んでる論文に出てくる音は正弦波だ。正弦波というのはy=sin(x)で表される波なわけだが、今仮にxを1刻みでデジタル化するとすると、sin(1)、sin(2)、sin(3)、…という値をひたすら計算しないといけない。

B: 何個くらい計算しないといけないんですか?

A: そうだな、音楽用CDと揃えるなら44.1kHz、すなわち1秒あたり44,100個だな。

B: 44,100個! たった1秒間で!

A: そう。それでもCDが出た頃にはこの個数が少なすぎるって叩かれたりもしたもんだ。ちなみにアナログ量をデジタル化することをサンプリングといい、1秒あたり何個の値に変換するかをサンプリング周波数という。周波数の単位はHzだ。

B: Hzって音の高さの単位だと思っていました。

A: そのHzは空気が1秒あたり何回振動しているか、だな。サンプリングと同じく1秒あたり何回?という値だから同じHzを単位として使う。振動の回数が多いほど人間には高い音に聞こえる。

B: ふむふむ。

A: さて、以上を踏まえてようやくプログラムの解説に入るぞ。まず6行目で変数freqにサンプリング周波数をセットしている。 続いて7行目のlinspace()関数だが、これはnumpyの関数で、1つめの引数と2つめの引数の間を等分割して長さが3番目の引数に一致する数列を生成する。 たとえばlinspace(0.0, 2.5, 6)とすると、最初が0.0で最後が0.5の数列、すなわちarray([0.0, 0.5, 1.0, 1.5, 2.0, 2.5])を得る。

B: array()ってなんですか?

A: pythonで行列を実現するnumpy.ndarrayクラスのインスタンスを作るメソッドさ。詳しくは機会があれば。これで7行目の意味がわかるか?

B: 0.0から1.0までを等分して44100個の数列を作るんだから…、ああ、y=sin(x)をサンプリングするためのxの値を作ってるんですね?

A: なかなかいい線いってるが、サンプリングする波はy=sin(x)じゃあない。sin(x)は周期2pi(piは円周率)、すなわちx=約6.28で一回分だ。2000Hzの音を作るにはx=1.0までの間に2000回繰り返さないといけない。

B: えーっと、じゃあ、じゃあ…どうすればいいんでしょう。

A: 結論から言うとy=sin(2000×2 pi x)とすればいい。周波数fの正弦波はy=sin(2 pi fx)だ。納得がいかなかったら数学の教科書を読んで自分で勉強してみなさい。さて、7行目でtに格納した数列の値ひとつひとつに対してy=sin(2000×2πx)を計算しなければいけないわけだが…

B: えっへん、リストのひとつひとつの値に繰り返し同じ処理をするにはforを使えばいいんですよね!ね!

A: ん。そこでforが頭に浮かぶのは偉い。確かにforを使っても出来るんだが、ここはnumpyの威力を実感してもらおう。 numpyパッケージのsin()関数は、numpy.ndarrayを引数に指定すると、行列の個々の要素に対してsin()を計算した結果を返してくれるんだ。さらに、numpy.ndarrayは+、-、*、/の演算子を使って個々の要素に同じ値を加減乗除する事が出来る。 だから、7行目で作ったtに対して単にsin(2000*2*pi*t)と書けばいいんだよ。

B: ええっ。なんかずるい!

A: 便利って言え。11行目では、さらにsin(2002*2*pi*t)として2002Hzの正弦波を作り、両者を足して括弧でくくって2で割ってる。両者の平均をとった形だね。さらにampという変数を掛け算しているが、これは正弦波の大きさを決めるための値だ。これを大きくするとボリュームが大きくなる。

B: 頭がパンクしそう…

A: で、12行目から先は11行目で作った波をwavファイルとして保存するための手続きだ。お決まりの形なんであまり解説する事はないんだが、まあざっと説明しておこう。 12行目で波形データを16bitの整数(-32768~32767の整数)に変換して変数dataに格納。wavファイルにするためには整数にしなきゃならん決まりだ。 13行目で保存用のファイルを開いている。wave.open()の第1引数は保存するファイル名で、第2引数は書き込み用に開くという指定だ。

B: mono.wavっていうファイル名で保存するんですね。

A: その通り。続く14行目でチャネル数を設定。1がモノラルで2がステレオ。15行目は波形データの範囲を決めるもので、12行目の整数化と関連している。まああまりよくわからなかったらとりあえず2にしておくと思っといてくれ。

B: またいい加減な…。

A: まずはよくわからなくても動かしてみることが大事だよ。自分で書いたプログラムがうまく動くと、もっと勉強しようという意欲がわくだろ? 16行目でサンプリング周波数を指定。17行目で実際にデータを書き込んで、18行目でファイルを閉じる。これで完成だ。

B: はあ。

A: 20行目以降はステレオのwavファイルを作る例だ。24行目以降はファイル名をstereo.wavにしている事とチャネル数を2にしている事以外は全く同じだな。 ポイントは21~23行目。21行目で左スピーカ用に2000Hzの正弦波、22行目で右スピーカー用に2002Hzの正弦波を作っている。

B: 11行目より分かりやすいですね。

A: 足したり割ったりしてないからな。それで23行目だが、ここがちょっと難しい。まず、array([wL,wR])で以下のような行列を作られる。

../_images/02-1-03.png

A: しかし、ステレオのwavファイルを作るためには以下のように並んでいなければならないんだ。

../_images/02-1-04.png

B: ええと、横長に並んでいるのを縦長に並べ替えないといけないってことですか。

A: 正確には行と列を入れ替えると言う。つまり、元の1列目が1行目、2列目が2行目、…という具合だな。並び替えのプログラムを書こうとするとちょっと面倒だが、 ここでもnumpyが助けてくれる。numpy.ndarrayにはtranspose()という関数がある。こいつはずばり、行と列を入れ替える機能を持っている。 array([wL,wR])を評価するとnumpy.ndarrayのインスタンスが生成されるので、それに.transpose()と続けてやると、この新しく生成されたインスタンスのtranspose()が呼ばれて行と列が入れ替わる。さらに左にw =とあるのだから、 このtranspose()で入れ替わったnumpy.ndarrayのインスタンスがwに格納される。晴れてwに目的の形に並んだデータが格納される。

B: ぜんぜんわからないんですけど、なんだかすごい…。

A: 全然わからんと言われるとさすがにどうかと思うが、まあいつも言うように少しずつ慣れていけばいい。 こんな風に一気に処理するんじゃなくて、ひとつ処理するたびに結果を変数に格納していくと初心者にはわかりやすいだろうな。 ちなみにwR、wLという変数に左右の音を格納したり、それをまとめてwという変数に格納したりしているのはまさにわかりやすさのためで、一気に以下のように書く事も出来る。

data = array([amp*sin(2000*2*pi*t),amp*sin(2002*2*pi*t)]).transpose().astype(int16)

B: もうわけわかりません…。

A: ちょっと悪乗りしすぎたかな。これでwavファイルを作るプログラムは完成だ。さらっと解説するつもりだったのに長くなってしまったな。 実行したらmono.wavとstereo.wavというファイルが出来るはずだからやってみな。

B: どれどれ…。おお、こんな風に聞こえるんですね。すげー。

A: とりあえずどんな音か確認するだけならこれで十分だね。 しかし自分で実験する場合はキー押しなどのタイミングに合わせて音を鳴らせなければいけない。次はpygameを使ってpythonから音を鳴らすのをやっとこうか。