音を拾い、回転を積む。「Sonidoverde」に見るマイク入力と極座標の造形
ブラウザのマイクを許可すると、黒い画面の中央に緑の細い線で描かれた幾何学模様が現れ、声や物音に合わせて形を変えます。今回取り上げるのは Camilo さん (@u175144) の「Sonidoverde」です。この記事の冒頭に作品が埋め込まれているので、読み進める前にまず動かしてみてください。環境によっては、画面を一度クリックしないと音が拾われないことがあります。操作はマウスの縦位置と、マイクに向かって出す音の二つです。コードを見る限り、マウスを上下させると線の形そのものが変わります。音が大きくなると、重なる図形の数が増えます。
この作品の見どころ
作品名の Sonidoverde は、私にはスペイン語で「緑の音」と読めます。作者の説明は添えられていないので意図は分かりませんが、画面を見るとその読みはしっくりきます。黒地に緑の線が一色だけ。色で遊ぶことをやめた代わりに、線の込み入り方だけで音を表しています。
私が惹かれるのは、図形が静止していないところです。コードを読むと、描くたびに少しずつ回転の量が増える仕掛けがあります。作品を開いてから時間が経つほど、同じ音でも模様のねじれが深くなっていくはずです。音に反応する作品の多くは、音が止めば元の姿に戻ります。この作品は時間の分だけ形が積もっていきます。静かな部屋で、ただ起動したまま眺めている時間にも意味がある作品だと感じます。
正直に言うと、コードの変数の絡み方はかなり入り組んでいて、何がどの見た目に効いているのかを一目では追えません。個人的には、そこがかえって良いと思っています。仕組みを完全に把握できないまま、マウスを動かして反応を確かめる。その手探りの感触が作品の一部になっているからです。ただ、狙った形を作りたい人には扱いにくいかもしれません。好みは分かれそうです。
コードの全体像
コードは mySketch という一つのタブにまとまっていて、関数は三つです。
- setup(): 最初に一度だけ動き、キャンバスとマイクの準備をします。
- draw(): 毎フレーム呼ばれ、音を測って図形の数を決め、並べて描きます。
- myShape(): 作者が自分で作った関数で、図形を一つ描きます。
音を扱う部分は、p5.js の追加ライブラリである p5.sound が担っています。マイクから音を取り込む p5.AudioIn、音を周波数ごとに分けて調べる p5.FFT、全体の音量を測る p5.Amplitude の三つを使います。FFT は、音を「低い成分がどれくらい、高い成分がどれくらい」と分けて数字にする道具です。ラジオのイコライザーで、棒が音に合わせて上下する様子を思い浮かべると近いと思います。
変数の宣言にも一つ特徴があります。mic や fft、spectrum といった変数は var が付いておらず、どの関数からでも読める置き方になっています。draw() で作った spectrum を myShape() の中で参照できるのは、このためです。
また、start、end、number、frms といった変数は用意されているものの、このコードの中ではどこにも使われていないように見えます。作る途中で試した跡なのかもしれませんが、これは推測です。読み解くうえでは、無視して構いません。
仕組みを読み解く
マイクの音を二つの数字にする
まず mySketch タブの setup() の中の、マイクと FFT をつなぐ部分です。
// Getting microphone input
mic = new p5.AudioIn();
mic.start();
// This allows us to generate a spectrum, and use the microphone as input
fft = new p5.FFT();
mic.connect(fft);
new p5.AudioIn() でマイクの入口を作り、mic.start() で聞き始めます。ブラウザはここで許可を求めてきます。次に new p5.FFT() で分析器を作り、mic.connect(fft) でマイクの音を分析器に流し込みます。水道管にたとえるなら、蛇口と浄水器をつないだ状態です。
この後ろで p5.Amplitude も作られ、同じマイクが入力として設定されます。toggleNormalize は音量の値を扱いやすい範囲にそろえる設定で、コメントにも「Get a better range」とあります。ただ、私はこの関数の細かい挙動までは確認できていないので、ここでは「音量を見やすい範囲に整える設定」と受け取るに留めます。
こうして毎フレーム、二種類の数字が手に入ります。全体の大きさを表す音量と、周波数ごとの強さを並べた配列 spectrum です。
mySketch タブの draw() の冒頭を見ます。
function draw() {
volume = 10 + amplitude.getLevel()*300
background(0);
//noFill();
translate(windowWidth/2, windowHeight/2);
strokeWeight(1);
spectrum = fft.analyze()
getLevel() で現在の音量を取り、300 倍して 10 を足したものが volume です。静かなときでも最低 10 は残ります。background() は画面を一色で塗りつぶす関数で、引数の 0 は黒です。毎フレーム黒で塗り直すので、前のフレームの線は残りません。
translate() は、描く基準点を動かします。ここでは画面の中央に基準点を移しているので、以降の座標は中央が (0, 0) になります。図形が画面の真ん中に出るのはこのためです。strokeWeight() は線の太さで、1 は細い線です。
最後の fft.analyze() が、周波数ごとの強さを 0 から 255 の数字の並びとして返します。配列の前の方ほど低い音です。
音量が図形の数を決める
次は、mySketch タブの draw() の中心にあるループです。
for (var j=0; j<map(volume, 0, 600, 0, 30); j++) {
var sp= spectrum[j];
rotate(frameCount/12000);
var f = j%15==0?255:0;
var dir = j%19==1?1:(sp);
myShape(2, f, dir, int(map(sp, 0, 1000, 0, 100))+j%1,149-(sp)*j);
}
map() は、ある範囲の数を別の範囲に写し替える関数です。引数は「値、元の範囲の下限、元の範囲の上限、新しい範囲の下限、新しい範囲の上限」の順です。ここでは 0 から 600 の volume を、0 から 30 に写しています。
ループは「j がこの値より小さい間」くり返されます。volume が最低の 10 なら写した値は 0.5 で、j = 0 の一回だけ図形を描きます。音が大きくなって volume が上がるほど回数が増え、図形が重なります。音量が最大に近づいても、volume はせいぜい 310 前後で、回数は十数回に収まりそうです。静かなときは一つ、騒がしいときは十数個。音量は、このループの回数として画面に現れます。
ループの中身を一行ずつ見ます。
sp = spectrum[j]:j番目の周波数の強さです。jは 0, 1, 2…と進むので、低い音の側から順に取り出していることになります。rotate(frameCount/12000): 座標系を少し回します。後で詳しく触れます。f:jが 15 で割り切れるときだけ 255、それ以外は 0 です。fill(f,0,0,0)の形で使われていますが、第四引数の 0 は透明度なので、塗りは透明です。見える色の違いにはならず、実際に見えているのは緑の線だけだと思います。dir:jを 19 で割った余りが 1 のときだけ 1、それ以外はspそのものです。myShapeの最後の二つの引数は、spを 10 分の 1 にして整数にした数と、149 - sp*jです。前者は模様の花びらの数のような役目、後者は図形の大きさの調整に使われます。
蛇足ですが、+j%1 は j を 1 で割った余りを足していて、整数の j では常に 0 です。結果には何も影響しません。
一つの図形を、角度と距離で描く
核心は myShape() の後半です。三角関数が出てくるので、先に考え方を述べます。ここで描かれる図形は、「中心から見て、どの方向に、どれだけ離れた点か」という指定を 100 個並べて作られています。時計の文字盤で、「3 時の方向に 5 センチ」「4 時の方向に 7 センチ」と点を打っていくイメージです。方向が角度で、離れ具合が距離です。cos と sin は、この角度と距離を、画面上の横と縦の位置に変換するために使います。
mySketch タブの myShape() の後半を見ます。
beginShape();
for (var i= 0; i<num; i++) {
var sp= spectrum[i];
rotate(frameCount/12000);
var angle = TWO_PI/num*i;
var minV = map(cos(dir*theta+dir*angle*100), -1, sp, 1, width*0.10);
var d = map(sin(angle*n), map(mouseY, 0, windowHeight, -100, 20), 2, minV, width*0.10+v);
var x = cos(angle)*d;
var y = sin(angle)*d;
vertex(x, y);
}
endShape(CLOSE);
beginShape() から endShape() までの間に vertex() で点を打つと、その点を結んだ図形になります。CLOSE を渡すと、最後の点と最初の点がつながって閉じた形になります。
angle は TWO_PI、つまり一周を 100 等分したものに i を掛けています。i が 0 から 99 まで進むと、ちょうど一周します。ここまでが「どの方向か」の部分です。
d が「どれだけ離れているか」です。sin(angle*n) は、角度に応じて -1 から 1 の間を波のように行き来します。n が大きいほど一周の間に波が何度も打たれるので、外側へ張り出す部分の数が増えます。先ほどの n は sp を 10 分の 1 にした値でした。つまり、その周波数が強いほど、模様の山の数が増える仕組みです。
この波を map() で距離に写し替えています。写した先の下限が minV、上限が width*0.10+v です。width*0.10 は画面幅の 10 パーセントなので、基本の大きさは画面幅の 1 割ほど。そこに v が足されます。v が 149 - sp*j だったので、j が大きく sp が強いと v は大きくマイナスになり、図形は縮むか、場合によっては裏返ります。
そしてマウスです。map(mouseY, 0, windowHeight, -100, 20) は、mouseY を画面の上端で -100、下端で 20 に写します。この値が、波を写し替えるときの元の範囲の下限になります。map() は範囲の外の値も計算してくれる関数なので、この下限が 2 に近づく下の方では、写し先を飛び越える計算になるはずです。マウスを上下させたとき形が急に変わる感覚は、この仕組みから来ていると私は読んでいます。ただ、細部までは追い切れておらず、確かではありません。
dir と theta を使う minV も同じ種類の計算で、こちらは波の下限を決めています。結局、図形の輪郭は「山の数」「山の高さ」「谷の深さ」を、音とマウスから別々に調整したものです。
回転が少しずつ積もっていく
最後に、rotate(frameCount/12000) に触れます。rotate() は、以降に描くものの向きを回す関数です。frameCount は、動き始めてから数えたフレームの回数です。毎回これを 12000 で割った小さな角度で回しています。
注目したいのは、この rotate が二か所にあることです。draw() のループの中と、myShape() の頂点ごとのループの中です。回転は取り消されず積み重なるので、一つの図形を描く間に 100 回、さらに図形の数だけ回されます。点を一つ打つたびに、座標系が少しずつ傾いていくわけです。
数字で見ます。frameCount が 1000 のころ、一回の回転は 1000÷12000 で約 0.083 ラジアン、角度にして 5 度弱です。これが一つの図形の中で 100 回続くので、一周以上のねじれになります。動かして 1 分後なら、一回あたり 17 度ほどになります。時間が経つほどねじれが強くなり、図形はただの花から、渦を巻いた複雑な模様へ変わっていくはずです。
draw() は毎フレーム最初から実行され、座標系の回転もリセットされます。ねじれの強さは、積み重なった frameCount だけが決めています。作品の経過時間がそのまま形に現れる仕掛けは、さりげないのに、この作品をいちばん特徴づけていると思います。
自分で改造してみる
draw()のrotate(frameCount/12000)とmyShape()の同じ行の12000を、3000や50000に変えてみてください。小さくするほど渦が早く強くなり、大きくするほどゆっくり、形が崩れずに保たれるはずです。二か所を別々の値にするのも面白いと思います。myShape(2, f, dir, ...)の最初の引数2を0に変えると、s == 0の分岐が働いて線が消えるはずです。塗りが透明なので、何も見えなくなるかもしれません。その場合はfill(f,0,0,0)の最後の0を100などにすると、塗りが見えてくるはずです。stroke(0,255,0)の数字を入れ替えると、緑以外の色になります。作品名に反するのですが、どう印象が変わるかを見る実験としては面白いはずです。draw()のvar number=50;の下にあるmap(volume, 0, 600, 0, 30)の最後の30を60にしてみてください。音量に対して図形の数が増えやすくなるはずです。重くなってスムーズに動かなくなるかもしれません。myShape()のvar num = 100を40や200に変えると、輪郭を作る点の数が変わります。少ないと角張り、多いと滑らかになるはずです。spectrum[i]のiもnumと連動するので、読む周波数の範囲も変わります。
OpenProcessing の作品ページで「Fork」すると、自分のアカウントにコードを複製して書き換えながら試せます。
まずは声を出さずに、マウスの縦位置だけを動かして、図形がどう変わるかを眺めてみるところから始めるのがよいと思います。
作品クレジット
- 作品
- 「Sonidoverde」
- 作者
- Camilo (@u175144)
- 公開日
- 2019/09/30 (OpenProcessing)
- ライセンス
- CC BY-SA 3.0
- 使用環境
- p5.js 0.9.0
作品は OpenProcessing の埋め込み機能で表示しています。著作権は作者の Camilo 氏に帰属します。 本文中のコードは、解説のために原作から抜粋したもので、内容は変更していません。
この記事のヘッダー画像は、OpenProcessing 上の本作品のサムネイルを 1024×576 に加工 (拡大・背景のぼかし合成) したものです。 この画像は原作と同じ CC BY-SA 3.0 で提供します。
作者について
Camilo (@u175144)
- OpenProcessing で公開している作品: 28 点
- OpenProcessing のプロフィールとほかの作品を見る ↗
作者情報は OpenProcessing の公開プロフィール (記事作成時点) に基づいています。


