Rustで作るノイズジェネレータ

プログラミングなどの集中が必要な作業をするときヘッドフォンで周囲の音を消す様にすると良いです。集中するのに適した音源は人によって様々ですが、基本的にはあまり主張が強く無い音が好まれますね。シンプルなピアノ曲や、環境音の入ったイージーリスニングなど、集中用・作業用曲ということでYouTubeでも沢山探せます。
経験的にプログラマではスラッシュメタルやデスメタルなどが良いという方も多いです。ノイジーな音楽は外部音のマスキング効果が高く、また疾走感がありますので、作業に良い影響を与える何らかの脳内物質が出ているのかもしれませんね。僕はノイズミュージック(Merzbowなど)が好きですが、作業中はシンプルなマスキング用のノイズを聞く様にしています。「ザー」というノイズです。

ノイズにもまた種類がありまして、マスキング用に好まれるのは主に「ホワイトノイズ」と「ピンクノイズ」このどちらかが多いと思います。
ホワイトノイズは全くのランダムな出来事を音に変換したものと考えてもらって良いでしょう。全くのランダムなものなので、周波数特性もフラットです。人間はおよそ20Hzから20kHzの音を聞くことができますが、ホワイトノイズはその全帯域に渡ってパワーが一定である様なノイズです。スペクトラムアナライザで見た時に大体全部が同じ高さでザラザラ動いているような形になります。
一方ピンクノイズは1/fノイズとも言われていて、周波数が上がるにつれてその帯域のパワー(音量と思ってOK)が小さくなるものです。1/fのfはfrequency=周波数ということで、周波数が2倍になれば反比例でパワーは半分になります。
ピンクノイズ、ホワイトノイズの様な名前は、このノイズの周波数帯における特性を、光のスペクトルと同様に考えた場合のアナロジーから来ています。つまりピンクノイズは低い周波数帯の方がパワーが大きいことになり、光では低い周波数は「赤」ですので、ホワイトノイズより赤に寄っているノイズということですね。
ホワイトノイズの特性とピンクノイズの特性
1/fというとどこかで聞いたなぁと思うかたもいらっしゃるかもしれません。少し前まで、扇風機に「1/fゆらぎ」といった機能が付いていることがありました(今もありますが数はあまり多くありません)。自然界を観察していると、この1/fの特性を見せるものが多いと言われています。扇風機のこのゆらぎ機能は、プログラムで生成した「1/fゆらぎ」で羽の回転をコントロールして、より自然に感じられる風を作ろうという狙いのものかと思います。
風の吹き方と比べるとマクロ/ミクロの違いがあり発生のメカニズムも異なりますが、ピンクノイズも自然の音に近いところがあります。ホワイトノイズが「シャー」といった人工的で耳障りな音であるのに対して、ピンクノイズの方は「ザー」という、滝の音などの、自然音を彷彿とさせる若干マイルドなノイズになっています。そのためホワイトノイズと比べるとマスキング効果は劣るものの、自然の音に近く長時間聴いた場合にも疲れにくいなどのメリットがあります。

YouTubeにもピンクノイズやホワイトノイズのトラックがあり再生回数もなかなかのものです。しかし自分は、この様なノイズをストリーミングで聞くのは、コンセプチュアルな部分で少し抵抗感があります。実はノイズをストリーミング再生をしているとき、人類の共有財産であるインターネットに不要な負荷をかけることになってしまうのです!(また、スマホで聞いているなら、普通の音楽を聴くより、単位時間あたりの「ギガの消費」も高くなります。)
ノイズはランダムであるため圧縮が効きづらく、データサイズが巨大になるからです。圧縮率の低い綺麗なノイズをストリーミングで聞きたいなと思っても、ネットワークの調子が悪いならば音がブチブチと途切れて、とても集中できたものではありません。そんな背景もありまして、今回はピンクノイズのジェネレータを作ることにしました。プログラムがその場でノイズを生成するので何時間でも連続して聞くことができ、インターネットやギガを無駄遣いせず、集中し続けられるという意識の高いプログラムです。
ピンクノイズのジェネレータを作る
1/fのノイズをどの様に生成するのか?これには色んな手段があります。今回は、2進数の各桁の値変化ごとに、その各桁に対応したランダムジェネレータの値を変化させるというメソッドを採用します。このメソッドがなかなか面白いので、少し詳しく紹介します。(だいぶ古いのでしっかりと覚えておらず申し訳有りませんが、おそらくマーチン・ガードナが最初に紹介した方法だと思います。)

まず適当な数のサイコロとサイコロの数と同じだけの桁数を持つ2進数をイメージします。例えば、サイコロを3個にするなら、3桁の二進数を用意します。そしてこの2進数の各桁がそれぞれのサイコロと関係づけられている様に考えておきます。ノイズを生成するには、時間に応じて、この二進数を1づつカウントアップしていきます。カウントアップによって2進数の各桁が0から1あるいは1から0に変化します。これによってある桁に変化があったら、そのタイミングで、当該の桁に関連づけられているサイコロを振るという様にします。そして、その時の全てのサイコロの出目の合計を出力とするのです。

これを繰り返した場合の出力が擬似的に1/fの揺らぎを持ったランダムになります。ここでサイコロの数(=2進数の桁数)が多いほど、この擬似ランダム現象の周波数特性のベースとなる最長周期は長くなっていきます。(その代わりコンピュータの計算負荷は少し高くなります。)まあオーディオ用のピンクノイズを生成するという目的の元ではそこそこの数で問題ないと思います。
二進数はコンピュータで扱いやすいところなので、プログラミングの題材としても良いですね。今回はRustを使って、このメソッドを実装してみました。
struct Dice {
hold: u32,
}
impl Dice {
fn new(seed: u32) -> Self {
Dice { hold: seed }
}
fn roll(&mut self) -> u32 {
self.hold = self.hold ^ (self.hold << 13);
self.hold = self.hold ^ (self.hold >> 17);
self.hold = self.hold ^ (self.hold << 15);
self.hold
}
}
struct NoiseGen {
counter: u32,
dices: Vec<Dice>,
}
impl NoiseGen {
fn new(seed: u32, dice_num: u8) -> Self {
let mut d = Dice::new(seed);
let mut dices: Vec<Dice> = Vec::new();
for i in 1..=dice_num {
dices.push(Dice::new(seed + i as u32));
}
let mut ret = NoiseGen {
counter: d.roll(),
dices,
};
ret.roll();
ret
}
fn roll(&mut self) -> u32 {
let count = self.counter + 1;
let mask = count ^ self.counter;
self.counter = count;
let cur = 0x01u32;
let mut total: u32 = 0;
for i in 0..self.dices.len() {
total += if (mask & (cur << i)) > 0 {
self.dices[i].roll()
} else {
self.dices[i].hold
} / self.dices.len() as u32;
}
total
}
}
これでノイズ生成部分ができました。これを音に変換して鳴らすには1秒間に44000回程、新しい値を生成して、それをサウンドバッファに書き込んでいくと言う作業が必要です。この部分を自分で作るのは結構大変ですが、RustにはcpalというAudioI/Oのライブラリがあって利用できます。
GitHub – RustAudio/cpal: Cross-platform audio I/O library in pure Rust
⇒https://github.com/RustAudio/cpal
gitHubのプロジェクト中でサンプルとしてサイン波を鳴らすものが紹介されています。こちらを少し修正するだけで済みそうですね。サイン波を生成しているところをコメントアウトして、代わりにノイズジェネレータをセットするだけです。
cpal/beep.rs at master · RustAudio/cpal · GitHub
⇒https://github.com/RustAudio/cpal/blob/master/examples/beep.rs
// let sample_rate = format.sample_rate.0 as f32;
// let mut sample_clock = 0f32;
// // Produce a sinusoid of maximum amplitude.
// let mut next_value = || {
// sample_clock = (sample_clock + 1.0) % sample_rate;
// (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin()
// };
let mut noi: NoiseGen = NoiseGen::new(12345, 11);
let mut next_value = ||{
(1.0f32 * noi.roll() as f32 / u32::max_value() as f32 - 0.5) * 2.0
};
サイコロの数(2進数の桁数)は11個としています。上で紹介した様にこの桁数が1/fの最長周期と関係しています。今回は音を生成しますので、人間の知覚の限界程度には周期を長くしておきたいですね。2進数の11桁は最大値が2047です、そのためこのカウンタを一周させるには2048回のカウントアップが必要です。1秒を44100サンプルとした場合、44100/2048=21.8周期/秒はおよそ20Hzになりますので、一応聴覚の下限程度になります。
実行してみましょう。いい感じのピンクノイズになっています。もしノイズを長時間聞きたい。という方がいらっしゃったら、是非こちらを参考にノイズジェネレータの作成にトライしてみてください。