フォルマント合成の回 で、プログラムは「あー」と歌えるようになりました。ノコギリ波をバンドパスフィルタに通すだけで母音が出る、というのがあの回の話です。気をよくして、次は「さ」と言わせたくなります。「さしすせそ」が言えれば、しゃべりまで手が届きそうです。

ところが、これが作れません。母音マップのどこを探しても「さ」はいないのです。フィルタの設定をどういじっても、出てくるのは「あ」と「お」の中間のようなあいまいな母音ばかり。当然で、「さ」の頭に付いている子音 s は、母音と同じ道具では作れない、材料から違う音だからです。今回と次回の2回に分けて、この子音の正体を追いかけます。前編は「さ」の仲間、擦る子音の話です。

「あー」は状態、「さ」は出来事

まず、母音と子音が何がそんなに違うのかを、波形で見てしまいます。

「あー」と伸ばした声の波形はどこを切り出しても同じ周期波形だが、「さ」の波形は前半がノイズ、後半が周期波形という並びになっている比較図

「あーーー」と伸ばした声は、上の段のように同じ形の波が延々と続きます。息の続くかぎり何秒でも伸ばせて、どの瞬間を切り出しても同じ形。つまり母音とは、口の形(フォルマント)を保てば維持できる定常状態です。フォルマント合成 がやったのは、この状態をひとつ作ることでした。

一方「さ」と言った声の波形が下の段です。前半にはギザギザのでたらめな波、つまりノイズが入っていて、後半で周期的な波に切り替わります。この前半のノイズこそが子音 s の正体で、「さ」という音は、ノイズから声への乗り換えという順番そのものなのです。音というより、始まりと終わりのある小さな出来事と言ったほうが近い。母音を作る道具が「状態を保つ」道具だったのに対して、子音づくりには「時間を設計する」発想が要ります。

音源そのものがノイズになる

では前半のノイズは、体のどこで生まれているのでしょうか。「すーーー」と s の音だけを伸ばしてみると分かります。このとき、のどに指を当てても震えていません。声帯は完全に止まっています。鳴っているのは、舌先を歯ぐきのすぐ裏に近づけて作った狭い隙間を、息が無理やり通り抜けるときの音です。狭いところを速い流れが通ると、流れが乱れて渦になり、シャーッという広い帯域のノイズが生まれます。蛇口を強くひねったときの水音や、風がすき間で鳴るのと同じ、乱流の音です。

このように、隙間の乱流を音源にする子音を摩擦音と呼びます。さ行の s、「し」の子音、は行の h、英語の f や th も仲間です。母音のときの声帯ブザーが「ブー」という周期的な音源だったのに対して、摩擦音では音源そのものがノイズに置き換わっている。source-filter モデル(音源×口の形のフィルタ、という声のとらえ方)の枠組みはそのままで、source の側が全とっかえになるわけです。

これはプログラムを書く側には朗報でもあります。ノイズなら簡単に作れるからです。毎サンプル、乱数を出力するだけで白色ノイズ、つまり全帯域が均等に入った「ザーッ」が手に入ります。あとは母音のときと同じように、フィルタで成形すればいい。実際、フォルマント合成の回 で「ささやき声でも母音が聞き分けられる」という話をしましたが、ささやき声とはまさに、声帯のブザーを息のノイズに差し替えた声のことでした。

s と sh を分けるのは、山の位置

同じ擦る音でも、「さ」の s と「し」の sh は明らかに違って聞こえます。s は鋭く高い「スー」、sh は少しこもった「シュー」。この違いは、ノイズのスペクトル(周波数ごとの成分の一覧)のどこにエネルギーの山があるかの違いです。

sは5〜8kHzの高い位置に山、shは2〜4kHzの低めの位置に山、fやhははっきりした山がない、という3段のスペクトル比較図

s は舌先と歯ぐきの間のせまい隙間で擦るので、山が高い周波数(おおよそ5〜8kHz)にできます。sh は隙間が少し奥に下がってやや広くなるぶん、山が低いほう(2〜4kHzあたり)へ降りてきます。口笛で口をすぼめると音程が変わるのと似て、擦る場所と隙間の広さが、ノイズの色を決めるのです。そして「ふ」の f や「はー」の h のように隙間がゆるい子音になると、山はぼやけて、弱く平らな息の音になります。

つまり摩擦音づくりとは、白色ノイズをバンドパスフィルタ(指定した帯域だけを通すフィルタ)に通して、山をどこに立てるかを選ぶ作業です。フィルタの回 では音の明るさを削って作りましたが、今回は削って子音を作る。中心周波数のつまみひとつで、s が sh になり、f になります。

もうひとつ、おまけのような一手があります。「すー」と擦りながら声帯も鳴らすと「ずー」になります。ノイズに声帯のブザーを重ねただけで、s は z に、「し」の子音は「じ」の子音に変わる。有声摩擦音と呼ばれる仲間で、部品としては「ノイズ+ブザー」の足し算です。

触ってみる

ここまでの話をそのまま触れるデモを用意しました。鳴っているのはいつも同じ白色ノイズで、フィルタの「山の位置」と「山の鋭さ」だけを動かせます。プリセットの s から sh へ、鳴らしたままスライダーを下ろしていくと、「スー」が「シュー」に化けていく途中も聴けます。有声化ボタンで声帯のブザーを重ねれば z の「ズー」に。下の段では、成形したノイズを母音 とつないで「さ・し・す・ず」と音節にしています。

うまく表示されないときは、デモを別タブで開く

音節ボタンを何度か押してみると、「さ」に聞こえはするものの、本物の声とはどこか違う、少し機械っぽい「さ」だと気づくと思います。実はここが今回の連載の隠れた主題で、母音は3本のフィルタでかなり本物らしく鳴るのに、子音は「それっぽい」止まりになかなか壁があります。人間の耳が子音の細部——ノイズの立ち上がり方や母音への乗り換えの滑らかさ——に驚くほど敏感だからです。この壁の正体は、時間の設計がもっとシビアになる後編(破裂音の回) でさらにはっきり見えてきます。

ところで、私事ですが音感トレーニングアプリ Harmonize を個人開発しており、いままさにフォルマント合成で「あー」と歌う声の音色を試作しています。まだアプリには入れていない開発中の音色で、作っているのも母音だけ。子音までは踏み込んでいません。楽器として使う声には定常状態だけで足りる、というのが正直なところですが、裏を返せば子音はそれだけ手間のかかる相手だということでもあります。

どう実現するか

擦る音の最小実装は、乱数とバンドパスフィルタだけです。母音のときの saw()noise() に入れ替わっただけで、配線は同じ形をしています。

/* 摩擦音:白色ノイズ → バンドパス(C風の擬似コード) */
for (n = 0; n < length; n++) {
    double src = noise();                  /* 毎サンプル乱数 ── 乱流の代役 */
    output[n] = bandpass(&bpf, src);       /* 山の位置=子音の種類 */
}
/* bpf の中心周波数: s なら 6500Hz あたり、sh なら 2900Hz あたり */

Web Audio API なら、ノイズを詰めた AudioBuffer をループ再生して BiquadFilterNode に通すだけです。

const buf = ac.createBuffer(1, ac.sampleRate * 2, ac.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;  // 白色ノイズ

const src = ac.createBufferSource();
src.buffer = buf; src.loop = true;
const bpf = ac.createBiquadFilter();
bpf.type = 'bandpass';
bpf.frequency.value = 6500;               // s の山。2900 にすると sh
bpf.Q.value = 4;                          // 山の鋭さ
src.connect(bpf).connect(master);
src.start();

「さ」と言わせるには、このノイズを 0.1 秒ほど鳴らしてから、音量をクロスフェードして母音側の配線(ノコギリ波+フォルマントのフィルタ3本)へ乗り換えます。上のデモの音節ボタンがやっているのはこれだけです。摩擦音は幸い、それ自体は「すーっ」と伸ばせる子音なので、乗り換えのタイミングが多少ラフでも音節として聞こえてくれます。

しかし、伸ばせない子音たちがいます。「た」「か」「ぱ」——口を一度ふさいで破裂させる音は、鳴っている状態というものがそもそも存在せず、数十ミリ秒の時間配置だけでできています。後編 では、この破裂音を無音・バースト・遷移の3部品から組み立てます。

参考文献

子音の前半戦は「ノイズの山をどこに立てるか」でした。まずはデモで、スライダー1本で「スー」が「シュー」に化ける瞬間を聴いてみてください。