まずは波形生成。アナログシンセでいうところのオシレータ(OSC)にあたります。
ソースコードはこちら。ひとまずデューティ比可変の矩形波と、正弦波、三角波に対応しています。
Java だと、オシレータの抽象クラスを作って、波形別に実装クラスを作って、パラメータをメンバ変数に持たせて…とかやるんですが、ここでは関数型らしく、パラメータを与えると、「パラメータに応じた波形を生成する関数」を返してくれる高階関数として実装しました。
例えば正弦波の場合はこんな感じ。
def sineWave(sampleRate: Double, tone: Double) = (z: Int) => { import scala.math._ sin(pInWave(sampleRate, tone, z) * 2.0 * Pi) }
サンプルレートと周波数を渡してあげると、時間変数 z (サンプリングレートの逆数を単位とする変数)を引数とする関数が帰ってきます。その関数を、例えばRange.mapに与えてあげると、一定時間内での波形をサンプルのリストとして取得できます。
scala> val sinConc=sineWave(100, 20) // サンプリングレート 100Hz, 音程20Hz sinConc: Int => Double = <function1> scala> Range(0, 100).map(sinConc) res0: scala.collection.immutable.IndexedSeq[Double] = Vector(0.0, 0.9510565162951535, 0.5877852522924732, -0.587785252292473, -0.9510565162951536, 0.0, 0.9510565162951535, 0.5877852522924736, -0.5877852522924734, -0.9510565162951536, 0.0, 0.9510565162951539, 0.5877852522924736, -0.5877852522924734, -0.9510565162951539, 0.0, 0.9510565162951539, 0.5877852522924736, -0.5877852522924734, -0.9510565162951539, 0.0, 0.9510565162951539, 0.5877852522924714, -0.5877852522924712, -0.9510565162951539, 0.0, 0.9510565162951539, 0.5877852522924714, -0.5877852522924712, -0.9510565162951539, 0.0, 0.9510565162951539, 0.5877852522924714, -0.5877852522924712, -0.9510565162951539, 0.0, 0.9510565162951539, 0.5877852522924714, -0.5877852522924712, -0.9510565162951539, 0.0, 0.9510565162951522, 0.58778525229247...
で、これらとは別に、モノラル音声を任意の定位でステレオの左右に振り分ける関数返す pan
と、与えられた係数をかける関数を返す vol
関数を作成しました。これらで得られる関数を andThen
で合成することにより、任意の定位、音量の音声波形を得る関数をクライアント側で作成できます。
クライアント側はこんな感じ。
// 音程440Hz、音量0.5、定位0.2 の矩形波関数 val sineConcrete = sineWave(FRAMES_PAR_SEC, 440.0) andThen vol(0.5) andThen pan(0.2) val samples = Range(0, FRAMES_PAR_SEC).flatMap(sineConcrete)
pan
で得られた関数の戻り値は、2ch分のサンプルを持つ Seq
なので、先ほどと違って Range.flatMap
で展開しています。
こんな風にしてえられた samples
を、前回作成した OutDevice
に出力すれば、めでたくパラメータ通りの音が鳴る、ということになります。
というところで今回は終了。次回はSMFライクなシーケンスデータで発音タイミングや音程を制御するあたりをやろうと思います。