p5.jsを使って波形についてふんわり勉強する


概要

下記の記事を読みました。
メディアアート系で重要な数式・概念 & p5.jsで遊ぶ
http://qiita.com/hp0me/items/8f43ad146744310664ae

楽しい。波形って見てるだけでも気持ちがいいです。
という事で自分もp5.jsを使って波形を書きながら波形(信号処理)についての知識を深めていこうと思います。

こんな人へ向けた記事です、半分は自己紹介。

  • 高校数学すら怪しい
  • 普段からJSを使う
  • シンセが好き

なんですが、信号処理と言いつつ今回は音は出ず波形を描画するのみです。
webAudioAPIを調べてみたんですが好き勝手な配列を渡して音が出せるわけではなく、waveテーブルを使うか倍音を加算する形での音作りになるようです。
今回やりたい事には合わず一先ず断念。暫くは描画のみで勉強していきます。

waveテーブル → PPGとかいいよね
倍音を加算 → オルガンって事?

と頭に浮かぶけど、数学とかよく分かってない知識の偏った人に最適な記事になっていると思います。
自分もその一人です。

なお今回のソースに関しては下記の書籍を大幅に参考にしています。
装丁も素敵で紙の書籍を持っているんですが、いまやPDF版がCCライセンスで配布されているようです。面白いので是非。
http://yoppa.org/blog/4299.html

では早速、今回作ったページのサンプルがこちら。
https://pppp606.github.io/p5_wave/

ソースは以下に。
主要部分のみqiita上に貼り付けていきますね。
https://github.com/pppp606/p5_wave/tree/qiita-no01

色々飛ばして波形生成

p5.jsの使い方とかjqueryでのUI部分とかは色々飛ばして波形生成部分のソースを見ていきます。

波形を決定付ける変数

まずは波の形を作る基になる変数です。
シンセを触ったりしていれば見たことある名前だと思います。説明過多かもしれません。

wave.js
var wave = {
  Set:{
    frequency:10,//周波数
    volume:50,//ボリューム
    phase:0,//初期位相

周波数(frequency)

ピッチです。440hzなら1秒間に440回波が上下します。
高ければ高い音が出るし、低ければ低い音が出ます。
特に説明の必要はなさそう。

今回であれば描画される横幅の800pxを1秒と想像してに周波数を設定しましょう
10hzであれば80pxで一往復する波形です。

ボリューム(volume)

これも特に説明いりませんね。
ボリュームについても今回は描画高さが120pxなのでゼロラインが60px部分にありそこからのマックスが60になります。ゲインと言う方が正しいんでしょうか。

位相(phase)

波形がスタートする位置です。x軸で波形が左右にずれる状態です。
一つの波形だけではあまり意味がありませんが、
今後、合成等々も作っていこうと思うので一先ず適当に付けておきました。
本来であれば周波数に対してのパーセントで設定出来る方が良い様に思います。

サンプリングレート

一秒間を何分割して音声を処理するかという話です。
今回は変数として持ってはいませんが横幅分の800がサンプリングレートに相当します。
細かいほど分解能が上がり滑らかでノイズの少ない波形が作れます。

44100とか目にするやつです。

波形を計算する為の準備

ここら辺から自分の勝手な理解が含まれてきます。
色んな人にごめんなさいと謝っておきます。
根本から全然間違えていたらご指摘ください!

一周が2πらしい

とりあえず色んな事は考えません。
とにかく波形が上から下まで一周するのに変化する角度が2πだと覚えます。
もーとにかくラジアンと呼ばれる型があり一周期で2πだと覚えました。
必要な知識は必要になった時に考えましょう。

サンプリング数で割る

今回の処理で言えば左端から右端まで800pxがサンプリング数です。
この800pxに対して周波数が設定され、例えば10hzであれば80pxで波形が一周する事になります。
80pxに対してのラジアンは2πになるので、単純に割ればx軸1pxに対しての変化量が求められます。

wave.js
wave.Set.diff = TWO_PI * wave.Set.frequency / width;

波形を作る

早速波形を作りましょう。
時間変化(x軸)に対しての波の高さ(y軸)を求めていき、隣同士を繋ぎ合わせると波形になります。

wave.js
  //周波数でサンプリング数で割る
  wave.Set.diff = TWO_PI * wave.Set.frequency / width;
  //サンプル(横幅)分繰り返す
  for (var i = 1; i < width; i++) {
    var y = wave.getY();
    //波形描画
    line(i-1,lastpoints,i,y);
    lastpoints = y;
  }
wave.js
  getY:function(){
    var y;
    this.Set.phase += this.Set.diff;
    while (this.Set.phase > TWO_PI) {
      this.Set.phase -= TWO_PI;
    }
    switch (this.Set.type) {
      case "sin"://サイン波
        y = Math.sin(this.Set.phase);
        break;
      case "saw"://ノコギリ波
        y = this.Set.phase / PI - 1;
        break;
      case "squ"://矩形波
        y = this.Set.phase < PI/4 ? -1 : 1;
        break;
      case "tri"://三角波
        y = this.Set.phase < PI ? -2 / PI * this.Set.phase + 1 : 2 / PI * this.Set.phase - 3;
        break;
    }
    //センターラインの位置とボリュームで調整
    y = y * this.Set.volume + height/2;
    return y;
  }

自分なりに解説をしていきます。

準備

先ずはこの後の計算で使う、ラジアンの現在値(this.Set.phase)を求めます。
上で求めたラジアンの変化量を加算していき、一周したら元に戻しています。

wave.js
this.Set.phase += this.Set.diff;
while (this.Set.phase > TWO_PI) {
  this.Set.phase -= TWO_PI;
}

後処理について

この後、波の計算をするのですが波形が一番上に張り付く状態がy==1、下がy==-1、センターラインがy==0のだと考えてください。
最終的にボリュームを掛けるのですが、後ほど説明します。

各波形の計算

ここから作りたい波形により計算方法が変わってきます。
一番楽しい部分です。

サイン波

wave.js
y = Math.sin(this.Set.phase);

これを書いてみてsinってのは角度型(ラジアン)をキャストする関数なんだと理解したんですが合っていますか?
三角関数は型変換だよと、中学生当時に教えてくれればすんなり理解出来たかもしれません。数学の前にプログラミングを教えればいいのに。
そもそも間違っていたらごめんなさい。

ノコギリ波

wave.js
y = this.Set.phase / PI - 1;

位相同士の割り算で斜めに進んでいって =1 になるタイミングで真上に戻る感じですね。
2πではないのは途中で入る垂直線部分で1π分変化するからという理解であっていますでしょうか。
後ろのマイナス1はセンターラインを中心に持っていく為の調整です。

矩形波

wave.js
y = this.Set.phase < PI ? -1 : 1;

解りやすいですね。this.Set.phase < PI で上下どちらかに張り付く状態です。
半周期毎に上下が入れ替わります。

デューティ比

this.Set.phase < PI/4 ? -1 : 1;

反転する位置をずらすとデューティ比が替わります。
ファミコン音源とかで見るやつですね。
チップチューンでいうと今度擬似三角波とかも作ってみようと思います。

三角波

wave.js
y = this.Set.phase < PI ? -2 / PI * this.Set.phase + 1 : 2 / PI * this.Set.phase - 3;

若干解り辛い!ぱっと想像は出来ていないのですが、一次関数にプラスマイナス付けて傾かせている状態って事であってますかね。

ボリュームの計算

一先ず上を1、下を-1として計算をしてきましたが、これに対してボリュームを掛けます。
センターラインで調整する為にマックス値の半分を足しています。

wave.js
y = y * this.Set.volume + height/2;

今日はここまで

一先ず波形が出来ました。やっぱり楽しい。
信号処理とか調べると、数式が出てきて付いていけなくなる人と一緒に理解を深められると嬉しいです。

今後の課題と目標

  • エイリアスノイズ対策(オーバーサンプリング)
  • 合成させる
  • 変調させる(LFO、FM)
  • エフェクトを作る(空間系とかコンプとかビットクラッシャーとか)
  • そもそも音を出す(jsに拘らなくても)
  • UIをきれいにする

またそのうち更新していこうと思います。