shader で音を鳴らして遊んでみた。
engineering

shader で音を鳴らして遊んでみた。

この記事はDeNA Advent Calendar 2020の8日目の記事です。

Sound Shader (Audio Shader) とは?

特定のプログラム、フレームワーク、API 等は無く、 コンピューターグラフィックスの為の GLSL (今回はWebGL) を使って音声を生成する呼称のようなモノのよう。

Shadertoy で音が流れるのをどうやっているのかが気になったのがきっかけで調査。

やり方

キャンバスに WebGL を使いシェーダーで描画、描画結果から WebGLRenderingContext.readPixels() によりピクセルブロックを読み取り、結果の ベクトル vec2X を左チャンネル、Y を右チャンネルとして Web Audio API AudioBuffer に割り当て、音を再生。

描画時のキャンバスサイズやサンプリングレートの値を変更する事により生成する音の長さ等を変更可能。

例えば 512px × 512px のキャンバス で 44.1kHz のサンプリングレートとした場合。

512.0 * 512.0 / 44100.0 = 5.944308390022676

描画エリア幅 * 描画エリア高 / サンプリングレート = 秒数

1度のレンダリングで約6秒(5.94s)の音声データを生成。

試したやり方

こちら Shadertoy の Sound Shader を Three.js で実装してみた
大変参考になる記事を拝見し three.js を最新版にしつつ、改修した部分を中心に抜粋。

three.js 音割り当て部分 (抜粋)

色々端折ってます。

const DURATION = 6 // 再生秒数

const WIDTH = 512 // 描画エリア幅
const HEIGHT = 512 // 描画エリア高

const audioCtx = new window.AudioContext()
const audioBufferSourceNode = audioCtx.createBufferSource()

const samples = WIDTH * HEIGHT
const numBlocks = (audioCtx.sampleRate * DURATION) / samples

const renderCtx = renderer.getContext()

for (let i = 0; i < numBlocks; i++) {
  uniforms.blockOffset.value = i * samples / audioCtx.sampleRate // シェーダーに数値の割り当て
  renderer.setRenderTarget(target) // オフスクリーンレンダリング 
  renderer.render(scene, camera)

  const pixels = new Uint8Array(WIDTH * HEIGHT * 4)
  renderCtx.readPixels(0, 0, WIDTH, HEIGHT, renderCtx.RGBA, renderCtx.UNSIGNED_BYTE, pixels)  // 描画結果を取得

  const outputDataL = audioBuffer.getChannelData(0) // 音声左チャンネル割り当て
  const outputDataR = audioBuffer.getChannelData(1) // 音声右チャンネル割り当て
  for (let j = 0; j < samples; j++) {
    outputDataL[i * samples + j] = (pixels[j * 4 + 0] + 256 * pixels[j * 4 + 1]) / 65535 * 2 - 1
    outputDataR[i * samples + j] = (pixels[j * 4 + 2] + 256 * pixels[j * 4 + 3]) / 65535 * 2 - 1
  }
}

audioBufferSourceNode.buffer = audioBuffer // 音を割り当てて
audioBufferSourceNode.start(0) // 再生

実験 1 フラグメントシェーダー

Shadertoy や先程の参考ページ に倣って vec2 mainSound を定義。

参考サイトから更にシンプルな単音 (440Hz, A, ラ) を鳴らすのみのフラグメントシェーダーに変更

#ifdef GL_ES
precision mediump float;
#endif

#define PI 3.141592653589793
#define TAU 6.283185307179586

uniform float sampleRate;
uniform float blockOffset;

vec2 mainSound(float time){
  return vec2(sin(TAU * 440.0 * time));
}

void main(void) {
  float t = blockOffset + ((gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * 512.0) / sampleRate;
  vec2 y = mainSound(t);
  vec2 v  = floor((0.5 + 0.5 * y) * 65536.0);
  vec2 vl = mod(v, 256.0) / 255.0;
  vec2 vh = floor(v / 256.0) / 255.0;
  gl_FragColor = vec4(vl.x, vh.x, vl.y, vh.y);
}

実験 2 フラグメントシェーダー (抜粋)

先程の参考サイト のシェーダーに変更

左右で少し値を変更し和音にされているようです。

float tri(in float freq, in float time) {
  return -abs(1.0 - mod(freq * time * 2.0, 2.0));
}

vec2 mainSound(float time) {
  float freq = 440.0;
  freq *= pow(1.06 * 1.06, floor(mod(time, 6.0)));
  return vec2(
    tri(freq, time) * sin(time * PI),
    tri(freq * 1.5, time) * sin(time * PI)
  );
}

実験 3 フラグメントシェーダー (抜粋)

みんな大好き glsl-noise をかましたらどうなるかを興味本位で 実験 1 に追加してみました。

実際に音が少しノイジーになっていて面白い結果に。

#pragma glslify: snoise = require(glsl-noise/simplex/2d)

vec2 mainSound(float time){
  return vec2(snoise(vec2(sin(TAU * time * 440.0))));
}

実験 4 フラグメントシェーダー (抜粋)

実験 2 にも簡易的にノイズを追加

#pragma glslify: snoise = require(glsl-noise/simplex/2d)

float tri(in float freq, in float time) {
  return -abs(1. - mod(freq * time * 2., 2.));
}

vec2 mainSound(float time) {
  float freq = 440.0;
  freq *= pow(1.06 * 1.06, floor(mod(time, 6.0)));
  return vec2(
    snoise(vec2(tri(freq, time) * sin(time * PI))),
    snoise(vec2(tri(freq * 1.5, time) * sin(time * PI)))
  );
}

その他ノイズの種類を変えてみたり数値を変えてみたり最小限の変更で遊んで楽しみましたが大きな変化はなかったので割愛。

実験 5

最後に自身もシェーダーでゴニョゴニョした「株式会社 集英社DeNA プロジェクツ」に組み込んでみました。

株式会社 集英社DeNA プロジェクツ 2019年2月設立した、集英社とDeNAの共同出資会社のコーポレートサイト。 メインビジュアルでは、集英社のジャンプ的オノマトペとDeNA的デジタル感を組み合わせて、 両社が生み出す新しいエンターテインメントへの期待感を演出しています。 日本語・英語・中国語の3言語に対応。 https://shueisha-dena-projects.com ※今回の取り組みは 「株式会社 集英社DeNA プロジェクツ」 とは関連がありません。

こちらはプロトタイプ段階で音に合わせてオノマトペを出し分けて動かすという実装も試作しており、それを掘り起こし組み込んでみました。
オーディオビジュアライズ部分は当初 THREE.AudioAnalyser を使っていましたが、使用にあたり不要な部分が多かったので途中から生の Web Audio API に切り替え。

(new AudioContext()).createAnalyser() を使用してビジュアライズ。

音声部分に関してはリズムマシーンの様なパターンが欲しかったので、GLSLで音楽(まずは、ドラムだ) という大変参考になるページを少しカスタマイズして使用させていただきました。

他はシェーダー特有の狙わずに面白い結果が出る事に期待しただけです。

現状のアウトプットは

  • 音がページに一切マッチしていない
  • リズムに合っていなくて気持ち悪い

です、ご容赦いただきそっとしておいて下さい・・・

動画では、途中で音を止めてオノマトペのビジュアライズがなくなるところも収録しました。

※補足

  • マウスに対して背景が反応しているのはグラボによって処理を出し分けているので環境によっては動作しません。
  • FPS はパフォーマンス観点で敢えて 30 にしております。

まとめ

詳細説明ができれば良かったですが、自身が把握しきれませんでした。

シェーダーで音を生成すれば Shadertoy の様にシェーダーのみでオーディオビジュアライゼーションが出来て楽しい事が出来そう。
以上です。


この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!
また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!