4:サウンドの作成と加工(エンベロープ) p5.sound.js サウンド

ノイズドラム エンベロープ

ホワイトノイズは、周波数スペクトルのあらゆる部分で等しいエネルギーを持つランダムなオーディオ信号です。
*
エンベロープは、時間 / 値のペアとして定義される、一連のフェード(増減)です。
*
このサンプルでは、p5.Envelopeを使ってp5.Noiseの出力振幅を制御することで、p5.Noiseをドラムのように再生します。p5.Amplitudeは、スケッチ内のすべてのサウンドのレベルを取得するので、この値を使って、エンベロープの動作を示す緑の矩形を描画します。

let noise, env, analyzer;

function setup() {
  createCanvas(710, 200);
  // デフォルトのノイズタイプは'white'、ほかに'brown'と'pink'がある
  noise = new p5.Noise();
  // 振幅エンベロープを作成
  env = new p5.Envelope();
  // attackTime, decayTime, sustainRatio, releaseTimeを設定
  env.setADSR(0.001, 0.1, 0.2, 0.1);
  // attackLevel, releaseLevelを設定
  env.setRange(1, 0);

  // p5.Amplitudeは、setInput()メソッドで入力を指定しない場合、
  // スケッチのすべてのサウンドを分析する
  analyzer = new p5.Amplitude();

  // ボタン
  const button = setButton('CLICK', {
    x: 600,
    y: 80
  });

  button.mousePressed(() => {
    // noiseを使って再生
    noise.stop();
    noise.start();
    env.play(noise);
  });
}

function draw() {
  background(0);

  // p5.Amplitudeアナライザーからの音量測定値を得る
  let level = analyzer.getLevel();

  // levelを使って緑の矩形を描画する
  let levelHeight = map(level, 0, 0.4, 0, height);
  fill(100, 250, 100);
  rect(0, height, width, -levelHeight);
}

function setButton(label, pos) {
  const button = createButton(label);
  button.size(100, 100);
  button.position(pos.x, pos.y);
  return button;
}

ボタンをクリックすると、ドラムの音が聞こえ、緑の矩形が上下します。

解説

ここでは、ノイズ音をp5.Noiseオブジェクトで作成し、それをp5.Envelopeオブジェクトで操作して、ドラムを叩いたときのような音にして再生しています。p5.Envelopeオブジェクトを使用すると、音量の細かな制御を行うことができます。

ノイズ

ノイズとは一定の周期を持たない波形を言い、再生すると文字通りノイズ音が聞こえます。ノイズはp5.Noiseオブジェクトで作成できます。

Cakewalk by BandLabに組み込んだシンセサイザー「Synth1」では、[Oscillators]にあるノイズボタンでノイズが作成できます。

下図は、Synth1で作成したノイズのmp3ファイルをAudacityで開き、波形を拡大表示した画面です。波形が不規則に上下しているのが分かります。

エンベロープ

英語のenvelopeは封筒の意味で、包み込むもの、というニュアンスがあります。ここで言うエンベロープは、音の鳴り始めから鳴り終わりまでの音量の変化を表す曲線を言います。p5.Envelopeはこの曲線を操作します。

前出のAudacityにもエンベロープを操作する機能がついています。

[エンベロープツール]ボタンをクリックして、サウンドの波形上をドラッグすると、下図のように、エンベロープの形を変えることができます。これにより音量の大小が変化します。

Audacityではこのように、封筒の端をつまんでいるかのようにして簡単にエンベロープが操作できますが、コードで行おうとすると、p5.Envelopeの詳細とエンベロープの概念を知る必要があります。

エンベロープの概念

音の鳴り始めから鳴り終わりまでの音量(振幅)の変化は、一般的に下図で表すことができます。赤い線は上のアニメーションで示すエンベロープの上半分に相当します。

ノートオンは音が鳴り出すタイミングで、たとえばピアノの鍵盤を押したときや、笛に息を吹き込んだときです。ノートオフは音の鳴り止みを始めるタイミングで、たとえば鍵盤を放したときや、笛に息を吹き込むのを止めたときです。

ノートオンで音が鳴り出すと、音量(つまり振幅)は0から急角度で増加し、最大音量(アタックレベル)に到達します。ノートオンからここまでの時間をアタックタイム(図のA)と言います。

その後音量は少し減衰します。ピアノの打鍵や笛吹きでは最初の音量をそのまま維持できないからです。この時間をディケイタイム(図のD)と言います。

減衰後音量は一定になります。このときの音量をサステインレベル(図のS)と言います。これはオルガンの鍵盤を押しつづけたり、笛を吹きつづけている状態の音量です。

そしてノートオフを迎えます。しかし空気の振動である音はすぐに鳴り止まず、弱まりながらつづきます。この余韻の時間をリリースタイム(図のR)と言い、最後の音量をリリースレベルと言います。

p5.Envelopeはこの概念をコードで表したものです。

リファレンスメモ

p5.Envelope

説明

エンベロープは、あらかじめ定義された、時間経過に渡る振幅の分布。エンベロープは通常、オブジェクトの出力音量(Attack, Decay, Sustain,Release(ADSR)と呼ばれる一連のフェード(増減))の制御に使用される。また、ほかのWeb Audio パラメータの制御にも使用できる。たとえば、p5.Envelopeは、osc.freq(env)のように発振器の周波数が制御できる

attackLevelとreleaseLevelの変更にはsetRange()を使用する。attackTime、decayTime、sustainPercent、releaseTimeの変更にはsetADSR()を使用する。

エンベロープ全体の再生にはplay()メソッドを使用し、ping可能なトリガーにはramp()メソッドを、またノート・オン、ノート・オフをトリガーするにはtriggerAttack()triggerRelease()メソッドを使用する。

シンタックス

new p5.Envelope()

フィールド

attackTime: エンベロープがattackLevelに達するまでの時間
attackLevel: アタック完了後のレベル
decayTime: エンベロープがdecayLevelに達するまでの時間
decayLevel:ディケイ後のレベル。エンベロープはリリースされるまでこのレベルに維持される
releaseTime:エンベロープのリリース部の継続時間
releaseLevel:リリース終了時のレベル

setADSR()

説明

従来のADSRエンベロープのような値を設定する。

シンタックス

setADSR(attackTime, [decayTime], [susRatio], [releaseTime])

パラメータ

attackTime 数値: エンベロープがアタックレベルに達する前の時間(秒単位)
decayTime 数値: エンベロープがディケイ/サステインレベルに達する前の時間(秒単位)(オプション)
susRatio 数値: 0から1のスケールで表されるattackLevelとreleaseLevelの比率。1.0はattackLevel、0.0はreleaseLevel。susRatioは、decayLevelと、エンベロープのサステイン部が維持されるレベルを決める。たとえば、attackLevelが0.4、releaseLevelが0、susAmtが0.5なら、decayLevelは0.2になる。attackLevelが(setRange()の使用によって)1.0に増加すると、decayLevelもそれに比例して、0.5になる(オプション)。
releaseTime 数値: 今からの秒単位の時間(デフォルトは0) (オプション)

setRange()

説明

エンベロープの最大(attackLevel)と最小(releaseLevel)を設定する。

シンタックス

setRange(aLevel, rLevel)

パラメータ

aLevel 数値: アタックレベル(デフォルトは1)
rLevel 数値: リリースレベル(デフォルトは0)

play()

説明

play()は、与えられた入力で動作を開始するようエンベロープに伝える。入力がp5.soundオブジェクト(つまりAudioIn、Oscillator、SoundFile)の場合、エンベロープはその出力音量を制御する。エンベロープはまた、Web Audio APIのAudioParamの制御にも使用できる。

p5.Noise

説明

Noiseは、ランダムな値を持つバッファを生成するオシレーター(発振器)の一種。p5.Oscillatorを拡張。

シンタックス

new p5.Noise(type)

パラメータ

type 文字列: ノイズのタイプ。’white'(デフォルト)か’brown’、または’pink’

ノイズについて

p5.Noiseでは、 ‘white’と’pink’、’brown’という3タイプのノイズが作成できます。後者2つはカラードノイズ(有色雑音)と呼ばれ、スペクトルの分布が周波数に対して平坦でないノイズです。

Audacityの[ジェネレーター]->[ノイズ]メニューを使うと、ホワイトノイズとピンクノイズ、ブラウンノイズが簡単に作成できます。

ホワイトノイズは、人間が聞くことができるすべての周波数を均一に含んだノイズで、激しい雨降りのような音に聞こえます。

[ホワイトノイズ]

ピンクノイズは、低音を強調したノイズで、ホワイトノイズよりも遠くで降っている雨の音に聞こえます。

[ピンクノイズ]

ブラウンノイズはさらに低音を強調したノイズで、滝つぼに激しく振っている雨の音のように聞こえます。

[ブラウンノイズ]

エンベロープのADSR(Attack, Decay, Sustain,Release)

次のコードは、setRange()でアタックレベルを1、リリースレベルを0に設定し、ADSR(Attack, Decay, Sustain,Release)の作用を波形で比較した例です。

let noise, env, analyzer;

// 最大音量
const attackLevel = 1.0;
// 最小音量
const releaseLevel = 0;

// 標準をサンプルのsetADSR()の引数にする
// attackTime, decayTime, sustainRatio, releaseTimeを設定
//  env.setADSR(0.001, 0.1, 0.2, 0.1);

// (1) attackTime比較
// 音の立ち上がりを決める。小さいほどサウンドの立ち上がりが早く、大きいほど遅くなる

// (1-1:立ち上がりが早い音)
let attackTime = 0;
let decayTime = 0.1;
let susPercent = 0.2
let releaseTime = 0.1;

// (1-2:立ち上がりが遅い音)
//attackTime = 1;
//decayTime = 0.1;
//susPercent = 0.2;
//releaseTime = 0.1;

// (2) decayTime比較
// 大きいほどゆっくりと減衰し、小さいほど速く減衰する

// (2-1:減衰なしの音)
//attackTime = 0.01;
//decayTime = 0.1;
//susPercent = 0.2;
//releaseTime = 0.1;

// (2-2:減衰ありの音)
//attackTime = 0.01;
//decayTime = 1;
//susPercent = 0.2;
//releaseTime = 0.1;

// (3) susPercent比較
// サステインレベル(減衰後の音量を維持する部分)の比率

// (3-1:持続しない音)
//attackTime = 0.01;
//decayTime = 0.1;
//susPercent = 0;
//releaseTime = 0.1;

// (3-2:持続する音)
//attackTime = 0.01;
//decayTime = 0.1;
//susPercent = 1;
//releaseTime = 0.1;

// (4) releaseTime比較
// 余韻。 大きいほど長く、小さいほど短くなる

// (4-1:余韻なしの音)
//attackTime = 0.01;
//decayTime = 0.1;
//susPercent = 0.2;
//releaseTime = 0;

// (4-2:余韻ありの音)
//attackTime = 0.01;
//decayTime = 0.1;
//susPercent = 0.2;
//releaseTime = 1;

let isOn = false;

function setup() {
    const canvas = createCanvas(100, 100);
    canvas.parent('sketch-holder');
    // p5.Envelopeオブジェクト
    env = new p5.Envelope();
    // ADSR値を設定
    env.setADSR(attackTime, decayTime, susPercent, releaseTime);
    // 音量変化の範囲を設定
    env.setRange(attackLevel, releaseLevel);
    // 音量解析器
    analyzer = new p5.Amplitude();

    const button = setButton('CLICK', {
        x: 600,
        y: 80
    });

    button.mousePressed(() => {

        // 複数回クリックできるように、ここで変数noiseをnullにする
        noise = null;
        // p5.Noise()オブジェクト
        noise = new p5.Noise();
        // ノイズ発振器(オシレータ)をスタート
        noise.start();
        // ノイズ発振器の振幅を0にして無音状態にする
        noise.amp(0);
        // エンベロープでノイズ発振器を再生
        env.play(noise);

        isOn = true;
    });
}

function draw() {
    background(0);
    if (isOn) {
        let level = analyzer.getLevel();
        plot(frameCount, level);
        if (frameCount > 100 && level <= 0) {
            isOn = false;
            noLoop();
        }
    }
}

function setButton(label, pos) {
    const button = createButton(label);
    button.size(100, 100);
    button.position(pos.x, pos.y);
    return button;
}

ADSRの比較は、EnvelopeのsetADSR()メソッドに渡す次の変数で行っています。1つめの例(1-1:立ち上がりが早い音)では、音の立ち上がりを早くするために、変数attackTimeを0にしています。なお、以降の例(1-2から4-2まで)も含めて、テストする変数以外の変数には、サンプルで設定されている数値(env.setADSR(0.001, 0.1, 0.2, 0.1))を使っています。

// (1-1:立ち上がりが早い音)
let attackTime = 0;
let decayTime = 0.1;
let susPercent = 0.2
let releaseTime = 0.1;

(1-1:立ち上がりが早い音)と(1-2:立ち上がりが遅い音)の変数設定では、下図の結果が得られました。

(2-1:減衰なしの音)と(2-2:減衰ありの音)の変数設定では、下図の結果が得られました。

(3-1:持続しない音)と(3-2:持続する音)の変数設定では、下図の結果が得られました。

(4-1:余韻なしの音)と(4-2:余韻ありの音)の変数設定では、下図の結果が得られました。

ノート エンベロープ

エンベロープは、時間/値のペアとして定義される、連続するフェード(増減)です。このサンプルでは、エンベロープを使ってオシレーターの出力振幅を制御することで、音階(ドレミ)を”演奏”します。

p5.Oscillatorは、内部に持つWeb Audio GainNode(p5.Oscillator.output)を通じて、その出力を送信します。このノードはデフォルトで0.5の定数値を持っていますが、osc.amp()メソッドでリセットできます。またこのサンプルでは、エンベロープがこのノードを制御して、音量つまみのように、振幅を上げ下げします。

let osc, envelope, fft;

// 音階(MIDIノート番号)の配列
let scaleArray = [60, 62, 64, 65, 67, 69, 71, 72];
// 音階の配列の何番めかを指す
let note = 0;
let isOK = false;

function setup() {
    createCanvas(710, 200);
    noStroke();

    // サイン波のオシレーターを作成
    osc = new p5.Oscillator('sine');
    // エンベロープを作成
    envelope = new p5.Envelope();
    // attackTime, decayTime, sustainRatio, releaseTimeを設定
    envelope.setADSR(0.001, 0.5, 0.1, 0.5);
    // attackLevel, releaseLevelを設定
    envelope.setRange(1, 0);
    // FFTを作成
    fft = new p5.FFT();

    // ボタンの繰り返しでサウンド再生のオン/オフを切り替える
    const button = setButton('START', {
        x: 600,
        y: 80
    });

    button.mousePressed(() => {
        if (!isOK) {
            soundStart();
            button.html('STOP');
        }
        else {
            soundStop();
            button.html('START');
        }
    });
}

function draw() {
    background(20);
    if (isOK) {
        playNote();
        drawSpectrum();
    }
}

//
function soundStop() {
    noLoop();
    isOK = false;
    note = 0;
}

// サウンドの再生開始
function soundStart() {
    // オシレーターをスタート
    osc.start();
    isOK = true;
    loop();
}

// 音階の配列の音を繰り返し再生
function playNote() {
    // 1秒に1回
    if (frameCount % 60 === 0 || frameCount === 1) {
        let midiValue = scaleArray[note];
        // MIDIノート値(midiValue)の周波数値を調べる
        let freqValue = midiToFreq(midiValue);
        // 周波数値をオシレーターに設定
        osc.freq(freqValue);
        // エンベロープでオシレーターを再生
        envelope.play(osc, 0, 0.1);
        // note値は0から7を循環する => ドレミ音が繰り返し再生される
        note = (note + 1) % scaleArray.length;
    }
}

// FFT.analyze()による周波数解析を描画
function drawSpectrum() {
    let spectrum = fft.analyze();
    for (let i = 0; i < spectrum.length / 20; i++) {
        fill(spectrum[i], spectrum[i] / 10, 0);
        let x = map(i, 0, spectrum.length / 20, 0, width);
        let h = map(spectrum[i], 0, 255, 0, height);
        rect(x, height, spectrum.length / 20, -h);
    }
}

function setButton(label, pos) {
    const button = createButton(label);
    button.size(100, 100);
    button.position(pos.x, pos.y);
    return button;
}

サウンドを再生するには[START]ボタンをクリックし、停止するには[STOP]ボタンをクリックします。

解説

このサンプルでは、音を作成し、それを加工して再生し、その音を分析して数値を取り出し、その数値を使ってキャンバスに矩形を描画する、ということを行っています。この流れを図にすると、下図のようになります。

  1. まず、p5.Oscillatorで音の波を作成します(図の1)。
  2. その波に周波数の数値を与えると、音程が変化します。周波数の値では人間が分かりにくいので、ここではドレミ(音階)に対応したMIDIノート番号を使っています(図の2)。
  3. この音の波を、p5.Envelopeの”筒”に通すと、p5.Envelopeで加工したように音がドレミの音階で鳴ります(図の3)。
  4. この実際に鳴っている音をp5.FFTで分析します。p5.FFTはスペクトルという数値の配列を出力します(図の4)。
  5. この配列の数値を使って、矩形を描画します。

ここでは、mp3ファイルなど、何もないところからドレミの音階を持つ音を作成しています。これは、(頑張れば)シンセサイザーのようなものも作れるということを意味しています。以降では順番にこの流れを見ていきます。

音の波を作成するp5.Oscillator

p5.Oscillatorオブジェクト使うと、「1:音について p5.sound.js サウンド」で述べた音色の元になる音の波が作成できます。oscillatorとは”発振器”の意味で、オシレーターと訳されます。

基本的な波形であるサイン波、三角波、ノコギリ波、矩形波は次のように作成します。

// オシレーターを作成
osc = new p5.Oscillator('sine'); // サイン波
//osc = new p5.Oscillator('triangle');  // 三角波
//osc = new p5.Oscillator('sawtooth');  // ノコギリ波
//osc = new p5.Oscillator('square');      // 矩形波

また最初にp5.Oscillator()に何も渡さず、素のp5.Oscillatorインスタンスを作成し、後からsetType()メソッドで波のタイプを指定することもできます。freq()メソッドを使うと、周波数(波の詰まり具合、音程)が指定できます。

osc = new p5.Oscillator();
osc.setType('sine');
osc.freq(240);// デフォルトは440Hz(A音)

後はp5.Oscillatorインスタンスからstart()を呼び出すと、指定した波形の音が再生できます。次のコードは、サイン波で低いラ音を鳴らす例です。p5.Amplitudeを使って、振幅(音量)をグラフで表示しています。

// p5.Oscillator
// オシレーターのタイプの設定(音色)、周波数の設定(音程)

let osc;
let amplitude;
let isPlaying = false;

function setup() {
    // オシレーターを作成
    osc = new p5.Oscillator('sine'); // サイン波
    //osc = new p5.Oscillator('triangle');  // 三角波
    //osc = new p5.Oscillator('sawtooth');  // ノコギリ波
    //osc = new p5.Oscillator('square');      // 矩形波
    // 低いラ(A)音
    osc.freq(220);

    // 音量をグラフに表示する
    amplitude = new p5.Amplitude();

    // ボタンの繰り返しでサウンド再生のオン/オフを切り替える
    const button = setButton('START', {
        x: 550,
        y: 50
    });

    button.mousePressed(() => {
        if (!isPlaying) {
            OscStart();
            isPlaying = true;
            button.html('STOP');
        }
        else {
            OscStop();
            isPlaying = false;
            button.html('START');
        }
    });
}

function draw() {
    if (isPlaying) {
        // 音量をグラフで表示
        let level = amplitude.getLevel();
        plot(frameCount, level);
    }
}

// オシレーターをスタート
function OscStart() {
    osc.start();
    loop();
}

// オシレーターをストップ
function OscStop() {
    osc.stop();
    noLoop();
}

function setButton(label, pos) {
    const button = createButton(label);
    button.size(100, 100);
    button.position(pos.x, pos.y);
    return button;
}

[START]ボタンをクリックすると、サイン波、110Hzの低いラ音が鳴ります。

リファレンスメモ

p5.Oscillator

説明

-1.0と1.0の間で振動する信号を作成する。振動はデフォルトで、正弦波の形状を取る(‘sine’)。そのほかのタイプには、三角波(‘triangle’)、ノコギリ波(‘sawtooth’)、矩形波(‘square’)がある。周波数のデフォルトは毎秒440回の振動、つまり440Hz(ヘルツ)。これは’A'(ラ)音に等しい。

振動のタイプはsetType()で設定するか、またはp5.SinOscp5.TriOscp5.SqrOscp5.SawOscで特定のオシレータをインスタンス化することで設定する。

シンタックス

new p5.Oscillator([freq], [type])

パラメータ

freq 数値: 周波数の。デフォルトは440Hz(オプション)
type 文字列: オシレータのタイプ(オプション)。’sine'(デフォルトは)、’triangle’、’sawtooth’、’square’

メソッド

start()
オシレータをスタートする。オシレータがスタートするまでの時間(今からの秒数)を決めるオプションのパラメータも取る。

stop()
オシレータを止める。オシレータを止めるまでの時間(今からの秒数)を決めるオプションのパラメータも取る

amp()
0と1.0の間の振幅を設定する。またはオシレータなどのオブジェクトを渡して、オーディオ信号で振幅を変調する。

freq()
オシレータの周波数を値に設定する。またはオシレータなどのオブジェクトを渡して、オーディオ信号で周波数を変調する。

setType()
タイプを’sine’か’triangle’、’sawtooth’または’square’に設定する。

音の波をp5.Envelopeの”筒”を通して再生

次は、p5.Oscillatorで作った音の波を、p5.Envelopeで加工した"筒"を通して再生する方法を見てみましょう。

p5.EnvelopeのsetADSR()とsetRange()メソッドは、封筒の筒をつまんで変形させるような操作です。筒のこの変形により、筒の中を通過する音の波は、その変形に沿って変化します。

// p5.Oscillatorで作った音の波を、p5.Envelopeで加工した"筒"を通して再生

let osc;
let amplitude;
let isPlaying = false;

let envelope;

function setup() {
    // オシレーターを作成
    osc = new p5.Oscillator();
    osc.setType('sawtooth'); // ノコギリ波
    osc.freq(220); // 周波数

    // エンベロープを作成
    envelope = new p5.Envelope();
    // ADSRを設定
    envelope.setADSR(0.5, 0.5, 0.1, 0.5);
    // アタックレベルとリリースレベルを設定
    envelope.setRange(1, 0);

    amplitude = new p5.Amplitude();

    // ボタンの繰り返しでサウンド再生のオン/オフを切り替える
    const button = setButton('START', {
        x: 550,
        y: 50
    });

    button.mousePressed(() => {
        if (!isPlaying) {
            OscStart();
            isPlaying = true;
            button.html('STOP');
        }
        else {
            OscStop();
            isPlaying = false;
            button.html('START');
        }
    });
}

function draw() {
    if (isPlaying) {
        // 音量をグラフで表示
        let level = amplitude.getLevel();
        plot(frameCount, level);
    }
}

// スタート
function OscStart() {
    osc.start();
    // オシレータをエンベロープを通して再生
    envelope.play(osc, 0, 0.5);
    loop();

}

// ストップ
function OscStop() {
    osc.stop();
    noLoop();
}

function setButton(label, pos) {
    const button = createButton(label);
    button.size(100, 100);
    button.position(pos.x, pos.y);
    return button;
}

[START]ボタンをクリックすると、ノコギリ波、220Hzの低いラ音が鳴ります。今度はEnvelopeで音を加工しているので、立ち上がりの速さや余韻の長さに変化があります。

音の再生を開始するときには、Oscillatorのstart()を呼び出したときに、Envelopeの.play()を呼び出すようにします。Envelopeのplay()メソッドは、入力(Oscillator)の音量を制御します。

// スタート
function OscStart() {
    osc.start();
    // オシレータをエンベロープを通して再生
    envelope.play(osc, 0, 0.5);
    loop();
}

// ストップ
function OscStop() {
    osc.stop();
    noLoop();
}
p5.FFTのanalyze()で周波数スペクトルを得る

スペクトル(spectrum)はざっくり言うと、「グラフなどで軸が波長になっているもの」のことで、最も馴染みのあるのが、太陽光をプリズムに通すと得られる「光スペクトル」です(参考:「LEDのスペクトル(分光分布)とは?」)。

太陽光はプリズムによって「紫・藍・青・緑・黄・橙・赤」の7色の光(単色光成分)に分けられ、その色(光の波長)によって、下図のように並んで映されます(左ほど波長は短く、右ほど長い)。

一見無色に見える太陽の可視光線はさまざまな波長(周波数)を持つ色の混合であり、音もまたさまざまな周波数を持つ波形の混合です。この混合を周波数ごとに分けて並べたものがスペクトルです。p5.FFTのanalyze()は、このプリズムと同じことを音について行うメソッドです。

下図の左は人が発した声の波形で、右はその周波数スペクトルです(出典:ウィキペディア「周波数スペクトル」)。右のグラフの横軸は周波数(0~4000Hz)です。15秒かけてサンプリングした人の声全体は、このような周波数に分けられる、ということです。

// p5.FFTのanalyze()で周波数スペクトルを得る

let env, osc;
let isOn = false;

// グラフ描画用データを入れる配列
let xArray = [];
let yArray = [];

function setup() {
    const canvas = createCanvas(400, 300);
    canvas.parent('sketch-holder');
    textSize(20);

    // オシレータの作成と設定
    osc = new p5.Oscillator();
    osc.setType('sine');
    // 周波数を変えると、グラフの結果も変わる
    osc.freq(110);

    // p5.FFTオブジェクトの作成
    fft = new p5.FFT();

    // p5.Envelopeオブジェクトの作成と設定
    env = new p5.Envelope();
    env.setADSR(0.0001, 0.1, 0.2, 0.1);
    env.setRange(1, 0);
}

function draw() {
    background(0);
    // dキーが押されたとき
    if (isOn) {
        drawSpectrum();
    }
    fill(255, 255, 0)
    text('Press d key', 4, 20);
}

// キーが押されるたびに1回だけ呼び出される
function keyTyped() {
    if (key === 'd') {
        isOn = true;
        // オシレータをスタートさせ、エンベロープで再生する
        osc.start();
        env.play(osc);
    }
}

function keyReleased() {
    isOn = false;
    // オシレータを止める
    osc.stop();
    // 次回のグラフ描画のため配列を空にする
    xArray = [];
    yArray = [];
}

// 周波数スペクトルを描画する
function drawSpectrum() {
    // spectrum.lengthはデフォルトで1024
    const spectrum = fft.analyze();
    const len = spectrum.length;
    // 確認用
    print(spectrum);
    const space = 10;
    const w = 3;
    noStroke();
    // 塗りを緑に
    fill(0, 255, 0);
    // spectrum配列を、space飛ばしで走査
    for (let i = 0; i < len; i += space) {
        // 0-lenの範囲にあるiを、0-widthの範囲の値に変換する
        let x = map(i, 0, len, 0, width);
        // 0-255の範囲にあるspectrum[i]を、0-heightの範囲の値に変換する
        let h = map(spectrum[i], 0, 255, 0, height);
        // 矩形を描画
        rect(x * space, height, w, -h);
        // グラフを描画
        plot(i, spectrum[i]);
    }
}

下図は実行例です。画面をクリックしてフォーカスを移し、dキーを押すとサウンドが再生できます。下図をクリックするとサンプルページが開きます。

fft.analyze()が返すspectrum配列は1024個の振幅値を持っていて、その値のインデックス位置は人間が聞くことのできる周波数に対応しています。つまり、グラフの横軸の縦棒はそれぞれ、spectrum配列の対応するインデックス位置にある振幅値を表しているということです。fft.analyze()は瞬間瞬間の音を分析して、周波数の小さい順に振幅値をspectrum配列に入れています。

リファレンスメモ

p5.FFT
FFT(高速フーリエ変換)は、波形内の個々のオーディオ周波数を分離する分析アルゴリズム。

インスタンス化されたp5.FFTオブジェクトは、次の2つの分析タイプにもとづく配列を返すことができる。

FFT.waveform()は、時間領域(time domain)に沿った振幅値を計算する。配列のインデックスは、抽出時のその一瞬のサンプルに対応している。それぞれの値は、それをサンプリングした瞬間の波形の振幅を表す。

FFT.analyze()は、周波数領域(frequency domain)に沿った振幅値を計算する。配列のインデックスは、人間が聞くことのできる最低音から最高の音までの周波数(つまりピッチ)に対応している。それぞれの値は、その周波数スペクトルをひとかたまりとする振幅を表す。特定の周波数や、周波数のある範囲内での振幅の計測にはgetEnergy()を併用する。

FFTは、サンプルバッファーと呼ばれる、音の非常に短いスナップショット(その瞬間を切り取ったもの)を分析し、振幅測定値の配列(ビンと呼ばれる)を返す。配列はデフォルトで1024ビンの長さ。ビン配列の長さは変更できるが、FFTアルゴリズムを正確に機能させるには、16と1024の間の2のべき乗である必要がある。FFTバッファーの実際のサイズはビンの数の2倍なので、標準的なサンプリングレートでは、バッファーは2048/44100秒の長さになる。

シンタックス

new p5.FFT([smoothing], [bins])

パラメータ

smoothing 数値: 周波数スペクトルの滑らかさ。0.0 < smoothing < 1.0。デフォルトは0.8。(オプション) bins 数値: 結果の配列の長さ。16と1024の間のべき乗でなくてはならない。デフォルトは1024。(オプション)

メソッド

waveform()

説明

単一バッファーに含まれる振幅測定値のスナップショット(その瞬間を切り取ったもの)を表す振幅値(-1.0〜+1.0)の配列を返す。長さはビンに等しい(デフォルトは1024)。音の波形の描画に使用できる。

シンタックス

waveform([bins], [precision])

パラメータ

bins 数値: 16と1024の間のべき乗でなくてはならない。デフォルトは1024(オプション)
precision 文字列: 値が与えられると、結果を、通常の配列よりも高精度がFloat32配列で返す(オプション)

戻り

配列: 振幅値(-1〜1)の経時的な配列。長さはbinsに等しい

analyze()

説明

周波数スペクトルの振幅値(0〜255)の配列を返す。長さはFFTビンに等しい(デフォルトは1024)。配列のインデックスは、人間が聞くことのできる最低音から最高音までの周波数(ピッチ)に対応している。それぞれの値は、その周波数スペクトルをひとかたまりとする振幅を表す。getEnergy()を使用するときには、getEnergy()より先に呼び出す必要がある。

シンタックス

analyze([bins], [scale])

パラメータ

bins 数値: 16と1024の間のべき乗でなくてはならない。デフォルトは1024(オプション)
scale 数値: “dB,”の場合には、-140と0(最大)の間のデシベルfloat測定値を返す。そうでない場合には、0〜255の整数を返す(オプション)

戻り

Array: 周波数スペクトルのエネルギー(振幅/音量)値の配列。最低のエネルギー(無音)は0、最高は255。

p5.FFTのwaveform()で波形を得る

p5.FFTのwaveform()メソッドを使用すると、鳴っている音の瞬間瞬間の振幅値を得ることができます。値は配列で返され、デフォルトで1024個の振幅値が入っています。

// FFTのwaveform()

let sound, fft;
let timeCount = 0;
// グラフ描画に使う配列
let xArray = [];
let yArray = [];

function preload() {
    sound = loadSound('assets/marunouchi.mp3');
}

function setup() {
    const canvas = createCanvas(400, 300);
    canvas.parent('sketch-holder');
    textSize(20);
    stroke(255, 255, 0);
    strokeWeight(2);
    noFill();
    fft = new p5.FFT();
    sound.setVolume(0.2);

    // キャンバスのクリックで再生のオン/オフを切り替え
    canvas.mouseClicked(togglePlay);
}

function draw() {
    background(0);
    if (sound.isPlaying()) {
        drawWaveform();
    }
    text('click to play/pause', 4, 20);
}

// 1/60 秒に1回呼び出される
function drawWaveform() {
    const waveform = fft.waveform();
    const len = waveform.length;
    //print(waveform);
    // キャンバスにwaveform値を描画
    beginShape();
    // 1/60 秒の間に、1024回実行する
    for (var i = 0; i < len; i++) {
        let x = map(i, 0, len, 0, width);
        // waveformは-1から1の値。これを0から300の数値に換算する
        let y = map(waveform[i], -1, 1, 0, height);
        vertex(x, y);
        // iが1023である瞬間
        if (i === len - 1) {
            // waveformの末尾の値をグラフに描画
            plot(timeCount, waveform[i]);
            print(waveform[i]);
        }
        timeCount++;
    }
    endShape();
    // 適当なタイミングで止める
    if (timeCount > 200000) {
        noLoop();
        sound.stop();
    }
}

// サウンドの再生/停止、グラフの描画/停止を切り替える
function togglePlay() {
    if (sound.isPlaying()) {
        sound.pause();
    }
    else {
        sound.play();
        timeCount = 0;
        xArray = [];
        yArray = [];
        loop();
    }
}

下図は実行例です。画面をクリックすると曲が鳴り出し、その波形が黒いキャンバスに描画されます。この波形はその瞬間の再生音の波形です。一方左のグラフでは、横軸に時間を取り、振幅値を変化を描画しています。下図をクリックするとサンプルページが開きます。


なお上記で(無断で)使っている音はYoutubeで公開されている下記「椎名林檎 – 丸の内サディスティック/cover by MiyuTakeuchi」のイントロ部です。

簡単シンセサイザー

p5.Oscillatorに波形と周波数を与えて原音を作り、p5.Envelopeを通すと、電子音楽で使用できそうな音が作成できます。p5.Oscillatorに”ド”の周波数を渡すと、ブラウザは”ド”の音を再生し、”レ”の周波数を渡すと”レ”の音を再生します。元になる音色はp5.Oscillatorに渡す波形のタイプで変わります。

このとき”ド”や”レ”の周波数では分かりづらいので、MIDI値を間に挟むこともできます。

MIDIは、Musical Instruments Digital Interfaceの略で、1981年に策定された電子楽器同士を接続するための世界共通規格です(参考:「今さら聞けない、「MIDIって何?」「MIDIって古いの?」」)。

しかし今必要なのは、MIDIの規格で決まっているMIDIノート番号です。これは、たとえばピアノの鍵盤の真ん中のドなら60番で、その右のレなら62番です。p5.sound.jsにはこの番号を周波数に変換するmidiToFreq()関数があるので、いちいち周波数を調べてp5.Oscillatorに与えなくても、60や62(=レ)をmidiToFreq()に渡しそれをp5.Oscillatorに指定することで、ドレミの音を鳴らすことができます。

下記の表は「MIDIノート番号と音名、周波数の対応表」からの抜粋です。

周波数 音名 MIDIノート番号 備考
261.6 ド(C) 60 鍵盤の真ん中のド
293.7 レ(D) 62
329.6 ミ(E) 64
349.2 ファ(F) 65
392.0 ソ(G) 67
440.0 ラ(A) 69 p5.Oscillatorのデフォルト
493.9 シ(B) 71
523.3 ド(C) 72 周波数は低いドの倍

C、Dというのはドレミの音階です。ドレミファソラシドはCDEFGABDで表されます。MIDIノート番号は半音上がると1大きくなり、全音上がると2大きくなります。したがって真ん中のドのシャープ(C#)は61番になります。ミからファ、シからドへは1音上がるだけなので(E#やB#はない)、1だけ大きくなります。

次のコードはボタンのクリックでドレミファソラシドの音階を再生します。適切なタイミングで適切なボタンをクリックすると、ハ長調(C調)の曲が演奏できます。

// MIDI値を周波数値に変換して、その周波数の音を鳴らす

let osc, envelope;

function setup() {
    createCanvas(710, 200);
    background(20);

    // サイン波のオシレーターを作成
    osc = new p5.Oscillator();
    osc.setType('sine');
    //osc.setType('triangle');
    //osc.setType('sawtooth');
    //osc.setType('square');
    // エンベロープを作成
    envelope = new p5.Envelope();
    envelope.setADSR(0.001, 0.5, 0.1, 0.5);
    envelope.setRange(1, 0);

    // setButton()関数に、ラベル、位置、MIDI値を渡して、ボタンを作成
    const button60 = setButton('ド(C)', {
        x: 50,
        y: 80
    }, 60);
    const button62 = setButton('レ(D)', {
        x: 110,
        y: 80
    }, 62);
    const button64 = setButton('ミ(E)', {
        x: 170,
        y: 80
    }, 64);
    const button65 = setButton('ファ(F)', {
        x: 230,
        y: 80
    }, 65);
    const button67 = setButton('ソ(G)', {
        x: 290,
        y: 80
    }, 67);
    const button69 = setButton('ラ(A)', {
        x: 350,
        y: 80
    }, 69);
    const button71 = setButton('シ(B)', {
        x: 410,
        y: 80
    }, 71);
    const button72 = setButton('ド(C)', {
        x: 470,
        y: 80
    }, 72);

}

// ボタンのマウスプレスで呼び出される関数
// 渡されたMIDI値を周波数に変換し、その周波数で音を再生する
function playNote(midiValue) {
    // MIDIノート値(midiValue)の周波数値を調べる
    let freqValue = midiToFreq(midiValue);
    print(freqValue);
    // 周波数値をオシレーターに設定
    osc.freq(freqValue);
    osc.start();
    // エンベロープでオシレーターを再生
    envelope.play(osc, 0, 0.1);
}

// ボタンを作成し、ラベルと位置、イベントハンドラを設定する関数
// マウスプレス時には、playNote()関数にMIDI番号を渡してこれを呼び出す
function setButton(label, pos, midiNumber) {
    const button = createButton(label);
    button.size(50, 100);
    button.position(pos.x, pos.y);
    button.mousePressed(() => {
        let midiValue = midiNumber;
        playNote(midiValue);
    });
    return button;
}

osc.setType()で設定する波のタイプを変えると、音色が変わります。またEnvelopeのsetADSR()に渡す値を変えると聞こえる音の印象が変わります。

リファレンスメモ

midiToFreq()

説明

MIDIノート値の周波数値を返す。一般的なMIDIでは、真ん中のCが60、C#が61、Dが62など、整数として扱う。オシレータで曲の周波数を作成するときに役立つ。

シンタックス

midiToFreq(midiNote)

パラメータ

midiNote 数値: MODIノート番号

戻り

Number: 与えられたMIDIノートの周波数値

オシレータの周波数

Oscillatorを制御し、FFTを使って波形を表示します。mouseXは周波数に、mouseYは振幅にマッピングしています(マウスが右に行くほど音程は高く、上に行くほど音量が大きくなる)。

let osc, fft;
let isPlaying = false;

function setup() {
    const canvas = createCanvas(720, 256);
    canvas.mouseClicked(togglePlay);

    // new p5.Oscillator('triangle')と同じ
    // 三角波のオシレータを作成、周波数はデフォルトの440Hz
    osc = new p5.TriOsc();
    // 振幅値(音量)を設定
    osc.amp(0.5);
    // FFTを作成
    fft = new p5.FFT();
    strokeWeight(5);
}

function draw() {
    background(255);
    if (isPlaying) {
        // 鳴っている音の波形を得る
        let waveform = fft.waveform();
        // waveform配列を使って図形を描く
        beginShape();
        for (let i = 0; i < waveform.length; i++) {
            let x = map(i, 0, waveform.length, 0, width);
            let y = map(waveform[i], -1, 1, height, 0);
            vertex(x, y);
        }
        endShape();

        // マウスのX位置にもとづいて、オシレータの周波数を変える
        let freq = map(mouseX, 0, width, 40, 880);
        osc.freq(freq);
        // マウスのy位置にもとづいて、オシレータの振幅を変える
        let amp = map(mouseY, 0, height, 1, 0.01);
        osc.amp(amp);
    }
}

// サウンドの再生/停止を切り替える
function togglePlay() {
    if (isPlaying) {
        osc.stop();
        isPlaying = false;
    }
    else {
        osc.start();
        isPlaying = true;
    }
}

画面のクリックで音の再生、図形の描画のオン/オフを切り替えます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA