Wavetable言語「UHM」の基礎知識

先日、FM AnthemというHive用のウェイブテーブル/プリセット集をリリースしたわけですが…

こちらの製品は、UHMというWavetable生成用のスクリプト言語を最大限に活用して作られています。UHMという言語環境は、ZebraやDivaを手掛けるu-he社がHiveのために用意したものです。

uhm

どんな波形を描くか、ノブをひねると波形がどう変化していくかを数式や論理式など決められた書式で書き、.uhmという拡張子のテキストファイルとして保存します。そしてそれをHive上で読み込めば、ウェイブテーブルを生成してくれるというシステムです。WAV形式でのエクスポートも可。この記事では、自分で波形を作りたい人のために、UHMの基礎知識を解説します。

基本構造

UHMでは最大で256フレームのWavetableを作成できます。ひとつのテーブルは、x座標が0〜1、y座標が-1〜1を基準としたグラフで表現され、x方向の解像度は2048サンプルです。

オーバービュー

ただしy座標に関しては、ある程度範囲からはみ出ていても、切り捨てられずに描画されるようになっていて、その点はSerumのParserと異なります。
x座標のことを「phase」と呼び、サンプルの1単位は「index」と呼ばれます。通常で主に利用するのは、「phase」の方でしょう(x座標という最も重要な変数が5文字と冗長であることには不満がありますが仕方ない)。

フレーム数について

もし256フレーム未満のWavetable、例えばスタティックな波形を5個だけ用意した5フレームのWavetableを作った場合には、HiveサイドではそれがWTposノブの0-100の中で均等に配分されます。これは、WAV形式のウェイブテーブルと同じです。

WTPosとの関係

フレームのない部分をどう対処するかはHive側で設定が可能で、デフォルトではクロスフェードで補完されます。上の例の場合、WTPosが12.5の時には、TriとSawをちょうど足して2で割った平均の波形が自動で生成されるということです。

簡単な書式

Wavetableを書くまでに必要な作業は少ないです。総フレーム数を指定して、グラフを書く。最低限この2行があれば動作します。


//スラッシュ2個でその行のそれ以降はコメントになります
NumFrames = 256 //フレーム数の指定
Wave "phase"	//phaseはいわゆるxのことなので、これはy=xのグラフを描いている

上の例の場合、256フレーム全てに同じSaw波が描かれます。

シンプルなSaw波

しかし見てのとおり、エリアのうち上半分だけしか使えていません。次のような記述を追加することで、-1〜1に整形ができます。


Spectrum lowest=0 highest=0 "0"

これはDC Offset Removal(直流電流のずれ除去)と呼ばれ、「波形が全く負の値を取っていない場合に、範囲を-1〜1に直してくれる」操作のようです(が、細かい挙動は私も把握していません)。

もちろん、はじめからきちんと範囲にあった数式を書くのでもよいでしょう。


Wave "2*phase-1"	//つまりはy=2x-1のグラフを描いている

select文と論理式

いわゆるIf文に相当するのが「select」で、①条件 ②当てはまる場合の処理 ③当てはまらない場合の処理 という3つの引数を持ちます。


Wave "select( (phase<=1/2) , 1 , -1)"	//グラフの左半分か右半分かを分岐してSqr波を描く

select文とSqr波

現状だと第1引数はカッコで括らないとエラーが出るようです。

論理式は常に値を返す

select文の引数として用いなくても、論理式は常に真なら1、偽なら0の値を返します。ですから極端な話、select文なしでもパルス波は描くことができます。


Wave "(phase<=1/2)"	//条件に当てはまる範囲は1、そうでなければ0の座標をとる
Spectrum lowest=0 highest=0 "0" //y座標の範囲を-1〜1に整形

ANDやORの表現

悲しいことに、現バージョンではANDを表す&&やORの||などは搭載されていません。それぞれ論理積・論理和というその名のとおり、論理式の積や和で表現するしかないという現状です。ANDは楽ですがORは面倒なので、アップデートされてほしいですね…。


Wave "(phase>=1/2)*(phase<3/4)"	//x座標が1/2以上かつ3/4未満なら真(1),他は偽(0)
Wave "( ((phase<=1/4)+(phase>=3/4)) >=1)"//x座標が1/4以下または3/4以上なら真(1),他は偽(0)
Wave "( ((phase<=1/4)+(phase>=3/4)) ==1)"//排他的論理和(XOR)の場合こう

ちなみにノットイコールの「!=」は対応しています。

frameを用いたモーフ

変数“frame”を用いることで、WTPosに応じて変化するマルチフレームのウェイブテーブルも作成できます。frameはフレーム番号を表す変数で、「フレームが進む(=WTposノブを回す)ごとに○○の値が上昇/下降していく」というモーフを表現する基礎となります。


Wave "sin( (2+2*frame/255)*pi*phase)"

上記は、サイン波のサイクルスピードを司る部分の数値を、フレームが進むごとに上昇させていくというごくシンプルなパターン。frameは0から255という巨大な幅で動くため、それを255で割ることで変化量を0-1に抑えています。

初めはy=sin(2πx)のグラフだったのが最終的にy=sin(4πx)になるので、WTposを捻ると波形が収縮していくようなモーフになりますね。

フレームによるモーフ

tableを用いたモーフ

しかし実際には、frameよりも“table”という変数を用いることの方が圧倒的に多いです。tableはframeと同様の役割を持つ変数なのですが、あらかじめ値が0〜1の範囲を取るようになっている点が異なります。先ほどの「わざわざ255で割る」という手間がすっかり省けるのです。


Wave "sin( (2+2*table)*pi*phase)" // ÷255を書く手間要らず

これは、SerumのParserで言うところの「y」に相当します。table変数にはframeに勝る大きな利点がもうひとつあるのですが、それは後述します。

ちなみに変化の仕方が線形か、対数的か、指数的かなども、もちろん数式の書き方次第で完全に調整が可能です。


Wave "sin( (2*2^table )*pi*phase)" // こう書くと変化は指数関数的になる

フレーム範囲の指定

例えば256フレームあるうち、前半と後半で動き方が異なるという場合も当然あるでしょう。その場合、“start”と“end”という属性を追加することで範囲の指定ができます。


Wave "sin( (2*2^table)*pi*phase)" start=0 end=127  //フレーム0〜127まではこの動き
Wave "sin( (4/2^table)*pi*phase)" start=128 end=255 //フレーム128〜255まではこの動き

こんな風に、フレームを好きなように分割して、好きな動きをすることができます。上記の場合、WTpos0-50でまず先ほどのようにサイン波が収縮し、50-100ではまた元のサイン波に戻るというモーフになります。

モーフ2

start属性は未指定の場合デフォルトは0、endの方はNumFramesで指定した最大フレーム数が初期値となります。ですから上の記述でいえば“start=0”や“end=255”は書いても書かなくても結果に影響しない冗長な部分になります。

フレーム範囲指定時のtableの挙動

このときtable変数は非常に賢い挙動をとります。フレーム範囲が0〜127と指定されたら、その範囲の中で0〜1まで動いてくれるのです。上の例では、フレーム0〜127でいったん0から1まで上昇し、128〜255では再度リセットしてまた0から1まで上昇するという形になります。つまりtableとは、「指定されたstart〜endのフレーム間で0から1まで動く変数」という、かなりフレキシブルな変数なのです。

実際に書いてみると分かりますが、このtable変数の動きは感動するほど便利で、フレーム内での動きを複雑にすればするほど、その恩恵が身に染みて感じられます。

start/endの逆向き指定

もしendの数値をstartよりも小さくした場合は、tableの値も逆向きに進むことになります。


Wave "sin( (4/2^table)*pi*phase)" start=127 end=0  //フレーム127から0に向かってtable値が上昇
Wave "sin( (4/2^table)*pi*phase)" start=128 end=255

この場合、start=127なのでこのときにtable値が0、end=0なので0フレーム目でtable値が1になるということで、結果的に先ほどと同じ波形が描かれます。

モーフ2

この逆向き指定を使わないと描けない波形というのはないですが、今回のようにWTpos50を事実上のスタート地点としてそこからWTpos0方向とWTpos100方向へ2種類のモーフを作っていくときなどは、この逆向き指定によって記述がクリアになることがあります。

xを用いた複数行にわたる記述

UHMの魅力は、複数行に記述を分けられること。前までの行で記述した内容を元に加工を加える場合には、“x”という変数を用います。“x”は、「その時点までの計算で作られた波形」を意味し、平たく言えば「前の行で書いたことを引き継ぐ」ための変数ということです。


Wave "asin(sin(2*phase*pi))/pi"	//これは周期的な三角波を描く数式です
Wave "x * (1-table)"		//それをテーブルが進むにつれフェードアウト
Wave "x + (2*phase-1)*table"	//そこにフェードインするSawを加算

例えばこんな風にすると、テーブルが進むにつれ三角波からノコギリ波にクロスフェードするウェイブテーブルが作成できます。これは要するに、一行にまとめて言えば以下と同じことなのですが…


Wave "(  asin(sin(2*phase*pi))/pi  )*(1-table) + (  2*phase-1  )*table"

ただ適度に行を分けることで可読性が高くなるほか、様々な応用が可能です。

blend属性による演算の指定

「x*」や「x+」と書かねばならないことが綺麗でないと感じる場合、“blend”という属性を用いることもできます。これは、現状描かれている波形に対して数式を足すのか、掛けるのかなどを指定できるものです。


Wave "asin(sin(2*phase*pi))/pi"		//まず三角波を描く
Wave "(1-table)" 	 blend=multiply	//この式を現在の波形に掛け算してという指示
Wave "(2*phase-1)*table" blend=add	//そこにこの式を足し算してという指示

blendは単に演算子の代わりとなるだけでなく、「その絶対値を掛ける」指示ができる“multiplyabs”や、「現在の波形と今この行で書いている波形のうち、値(y座標)が大きい方を採る」指示ができる“max”などがあります。

3つのバッファの利用

より複雑な波形を作ろうとしたときには、「いったんこの波形を作っておいて、それにコレを混ぜて…」というような作業をするための“一時的な波形置き場”が欲しくなります。

UHMでは波形を保管しておける場所を「バッファ」と呼び、mainバッファの他にaux1、aux2という予備の波形保管場所が存在します。保管場所は、“target”という属性で指定します。
例えば先ほどの三角波からノコギリ波へのクロスフェード波形は、aux1バッファを利用するとより可読性の高い形で記述が可能です。


Wave "asin(sin(2*phase*pi))/pi"	//まずは三角波
Wave "x * (1-table)"		//それをテーブルが進むにつれフェードアウト
Wave "(2*phase-1)" target=aux1	//aux1にひとまずSawを生成
Wave "x * table" target=aux1	//それをフェードイン加工
Wave "x + aux1"			//aux1で作ったものをmainに加算して合体

もちろんこの程度の波形なら5行に分けるまでもないですが、FM合成など煩雑な波形生成を行うときには、このaux1・aux2が非常に役立ちます。

WAVファイルへのエクスポート

UHMファイルは現状Hiveでしか読み込みができません。Exportというコマンドを使えば、WAVファイルにして書き出すことができます。


Export "../My WT/Hello UHM.wav" //一般的な形でパスの指定が可能

Exportの記述を含むUHMファイルをHiveがロードした際に、自動的に書き出しが行われます。パスの指定が間違っていると、何も生成されません。また同一パスのファイルが存在している場合は、警告なしで上書きされます。

書き出したWAVの互換性

書き出したWAVファイルは、Hiveはもちろんのこと、例えばVitalでもそのまま読み込みが可能でした。
一方Serumの場合メニューから直接読み込みでは正しくロードできず、ドラッグ&ドロップから“constant frame size”で読み込まねばなりませんでした。


ほか、簡単にエンヴェロープを生成できる機能や、LP/HP/BPフィルターをかける関数、好きなフレームの好きな座標の値を取得できるfi関数、Spectrumベースで行う加算合成など、UHMには魅力的な機能がたくさんあります。関数や変数についての詳細な一覧については、ユーザーガイドを参照してください。