<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />

  </head>
  <body>
    <script src="sketch.js"></script>
  </body>
</html>
html, body {
  margin: 20;
  padding: 0;
}
canvas {
  display: block;
  border: solid 1px;
}

/**
 * 波形: ノイズ
 * @param {number} t 時間 
 */
function waveNoiseFunc(t) {
  return Math.random() * 2 - 1;
}

/**
 * 波形: sin(2πft)
 * @param {number} t 時間
 * @param {number} freq 周波数 
 */
function waveSin2npiFunc(t, freq) {
  return Math.sin(2 * Math.PI * freq * t);
}

/**
 * 波形: cos(2πft)
 * @param {number} t 時間
 * @param {number} freq 周波数
 */
function waveCos2npiFunc(t, freq) {
  return Math.cos(2 * Math.PI * freq * t);
}

/**
 * 波形: フーリエ級数
 * @param {number} t 時間
 * @param {number} a0 定数関数
 * @param {number[]} aArray cos関数の定数項
 * @param {number[]} bArray sin関数の定数項
 * @param {number[]} baseFreq 計算のベースとする周波数
 */
function waveFourierSeriesFunc(t, a0, aArray, bArray, baseFreq = 441) {
  // cos値の計算
  let cosValues = [];
  for (let n = 0; n < aArray.length; n++) {
    const an = aArray[n];
    const freq = (n + 1) * baseFreq;
    cosValues.push(an * waveCos2npiFunc(t, freq));
  }
  // sin値の計算
  let sinValues = [];
  for (let n = 0; n < bArray.length; n++) {
    const bn = bArray[n];
    const freq = (n + 1) * baseFreq;
    sinValues.push(bn * waveSin2npiFunc(t, freq));
  }
  // フーリエ級数の形で計算して返却
  const cosSum = cosValues.length == 0 ? 0 : cosValues.reduce((sum, x) => sum + x, 0);
  const sinSum = sinValues.length == 0 ? 0 : sinValues.reduce((sum, x) => sum + x, 0);
  return a0 / 2 + cosSum + sinSum;
}

/**
 * フーリエ解析
 * @param {(t: number) => number} f 波形関数
 * @param {number} N 解析する定数項の数
 * @param {number[]} baseFreq 計算のベースとする周波数
 * @param {number} dotN 内積を行う回数(数値を上げると精度が向上する)
 * @returns a0, aArray, bArray
 */
function fourierCoefficients(f, N, baseFreq = 441, dotN = 100) {
  // 関数同士の内積
  // 2 * ∮[0→1]f(t)g(t)
  function dot_function(f, g, N) {
    const dt = 1 / N;
    let array = [];
    for (let t = 0; t < 1; t += dt) {
      array.push(f(t) * g(t) * dt);
    }
    return 2 * array.reduce((sum, x) => sum + x, 0);
  }

  // sin、cos関数との内積結果から係数を求める
  let a0 = dot_function(f, (t) => 1 / 2, dotN);
  let anArray = [];
  let bnArray = [];
  for (let n = 0; n < N; n++) {
    const freq = (n + 1) * baseFreq;
    anArray.push(dot_function(f, (t) => waveCos2npiFunc(t, freq), dotN));
    bnArray.push(dot_function(f, (t) => waveSin2npiFunc(t, freq), dotN));
  }
  return {a0, anArray, bnArray};
}

/**
 * ボタン類
 */
let playNoiseButton;
let playSinButton;
let playRectButton;
let playFoorierButton;

/**
 * テキスト表示値
 */
let a0Value = 0;
let anValueArray = [];
let bnValueArray = [];

/**
 * 再生中の波形データ
 */
let playWaveData = [];

/**
 * ノイズ音再生ボタン押下
 */
function pushNoisePlayButton() {
  playSoundAndCheckFourier(waveNoiseFunc);
}

/**
 * sin波再生ボタン押下
 */
function pushSinPlayButton() {
  playSoundAndCheckFourier((t) => waveSin2npiFunc(t, 441));
}

/**
 * 短形波再生ボタン押下
 */
function pushRectPlayButton() {
  let bArray = [];
  const bCount = 30; // この数を上げるほど短形波に近づく
  for (let n = 1; n <= bCount; n++) {
    let b = n % 2 != 0 ? (4 / (n * Math.PI)) : 0;
    bArray.push(b);
  }
  playSoundAndCheckFourier((t) => waveFourierSeriesFunc(t, 0, [], bArray));
}

/**
 * フーリエ波形再生ボタン押下
 */
function pushFoorierPlayButton() {
  playSoundAndCheckFourier((t) => waveFourierSeriesFunc(t, 0, [1, 2, 3], [1]));
}

/**
 * サウンド再生とフーリエ解析を行う
 * @param {(t: number) => number} waveFunc 波形関数
 */
function playSoundAndCheckFourier(waveFunc) {
  // サウンド再生
  playWaveData = playSound(waveFunc);
  
  // フーリエ級数を求める
  const checkFourierCount = 5;
  let result = fourierCoefficients(waveFunc, checkFourierCount);
  a0Value = result.a0;
  anValueArray = result.anArray;
  bnValueArray = result.bnArray;
}

/**
 * 初期化処理
 */
function setup() {
  createCanvas(ScreenWidth, ScreenHeight);
  // ボタン設定
  playNoiseButton = createButton("Play Noise");
  playNoiseButton.mousePressed(pushNoisePlayButton);
  playNoiseButton.position(ScreenWidth - (playNoiseButton.width), 20);
  playSinButton = createButton("Play Sin Wave");
  playSinButton.mousePressed(pushSinPlayButton);
  playSinButton.position(ScreenWidth - (playSinButton.width), 48);
  playRectButton = createButton("Play Rect Wave");
  playRectButton.mousePressed(pushRectPlayButton);
  playRectButton.position(ScreenWidth - (playRectButton.width), 76);
  playFoorierButton = createButton("Play Foorier Wave");
  playFoorierButton.mousePressed(pushFoorierPlayButton);
  playFoorierButton.position(ScreenWidth - (playFoorierButton.width), 102);
}

/**
 * 描画処理
 */
function draw() {
  background(255);
  drawGrid();
  drawWaveData(playWaveData);

  // テキスト描画
  noStroke()
  fill(0, 0, 0)
  textSize(18)
  text(`a0: ${Math.abs(a0Value).toFixed(1)}`, 20, 30)
  text(`a[]: ${anValueArray.map(x => Math.abs(x).toFixed(1)).join(', ')}`, 20, 56)
  text(`b[]: ${bnValueArray.map(x => Math.abs(x).toFixed(1)).join(', ')}`, 20, 82)
}


// ===== サウンド再生 =====

/**
 * サンプルレート
 */
const SampleRate = 44100;

/**
 * 再生時間(秒)
 */
const PlaySec = 1;

/**
 * 再生中の波形データ
 */
let bufferSource = null;

/**
 * サウンド再生処理
 * 参考: https://developer.mozilla.org/ja/docs/Web/API/BaseAudioContext/createBuffer
 * @param {*} func 波形関数
 */
function playSound(func) {
    // stop playing sound.
    if (bufferSource) {
        bufferSource.stop();
    }

    // create context.
    let audioCtx = new AudioContext();
    audioCtx.sampleRate = SampleRate;

    // create mono channel buffer.
    let buffer = audioCtx.createBuffer(1, PlaySec * SampleRate, SampleRate);
    let channelData = buffer.getChannelData(0);
    for (var i = 0; i < buffer.length; i++) {
        let playSec = i / SampleRate;
        channelData[i] = func(playSec);
    }

    // set play wave data.
    let playWaveData = channelData;

    // play sound.
    bufferSource = audioCtx.createBufferSource();
    bufferSource.buffer = buffer;
    bufferSource.connect(audioCtx.destination);
    bufferSource.start();

    return playWaveData;
}

// ===== グリッド描画・プロット =====

/**
 * 画面設定
 */
const ScreenWidth = 400;
const ScreenHeight = 400;

/**
 * 座標の表示範囲
 */
const MinGridValueY = -5;
const MaxGridValueY = 5;
const MinGridValueX = 0;
const MaxGridValueX = 0.005;

/**
 * グリッドの描画
 */
function drawGrid() {
    strokeWeight(2);
  
    let dx = MaxGridValueX / 10;
    let dy = MaxGridValueY / 10;
    for (let x = MinGridValueX; x < MaxGridValueX; x+=dx) {
      for (let y = MinGridValueY; y < MaxGridValueY; y+=dy) {
        // 縦線
        stroke(240, 240, 240);
        drawLineCanvas(x+dx, y, x+dx, y+dy);
        // 横線
        stroke(180, 180, 180);
        drawLineCanvas(x, y+dy, x+dx, y+dy);
      }
    }
  
    // 中央線
    stroke(120, 120, 120);
    drawLineCanvas(MinGridValueX, 0, MaxGridValueX, 0);
    drawLineCanvas(0, MinGridValueY, 0, MaxGridValueY);
  }
  
  /**
   * 波形データを描画
   * @param {number[]} waveData 波形データ
   */
  function drawWaveData(waveData) {
    strokeWeight(4);
    stroke(0, 0, 255);
  
    for (let i = 0; i < waveData.length; i++) {
      // 最大秒数を超えたらスキップ
      if (i / SampleRate > MaxGridValueX) {
        break;
      }
      if (i == waveData.length - 1) {
        break;
      }
      // 次のデータと繋げる
      let data = waveData[i];
      let time = i / SampleRate;
      let nextData = waveData[i + 1];
      let nextTime = (i + 1) / SampleRate;
      drawLineCanvas(time, data, nextTime, nextData);
    }
  }
  
  /**
   * 関数の描画
   * @param {(t: number) => number} func 関数 
   */
  function drawFunc(func) {
    strokeWeight(4);
    stroke(0, 0, 255);
    let dx = MaxGridValueX / 100;
    for (let x = 0; x < MaxGridValueX; x+=dx) {
      drawLineCanvas(x, func(x), (x+dx), func(x+dx));
    }
  }
  
  /**
   * 受け取った座標位置で線を繋げて表示する
   * @param {number} fromX 
   * @param {number} fromY 
   * @param {number} toX 
   * @param {number} toY 
   */
  function drawLineCanvas(fromX, fromY, toX, toY) {
    fromX *= ScreenWidth/(MaxGridValueX-MinGridValueX);
    fromY *= ScreenHeight/(MaxGridValueY-MinGridValueY);
    toX *= ScreenWidth/(MaxGridValueX-MinGridValueX)
    toY *= ScreenHeight/(MaxGridValueY-MinGridValueY);
    let offset = [0, ScreenHeight/2]; // Y軸は反転させる
    line(fromX+offset[0], ScreenHeight-(fromY+offset[1]), toX+offset[0], ScreenHeight-(toY+offset[1]));
  }

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.