Oscilloscope with Web Audio API + Vue.js

by bc_rikko

HTML

<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
  <form>
    <button type="button" @click="start">Start</button>
    <button type="button" @click="stop">Stop</button>
    <div class="scope-type">
      <h3>描画タイプ</h3>
      <label><input type="radio" value="svg" v-model="scopeType">SVG</label>
      <label><input type="radio" value="canvas" v-model="scopeType">Canvas</label>
    </div>
  </form>
  <div class="oscilloscopes">
      <svg
        v-if="scopeType === 'svg'"
        ref="scope"
        :width="size.width"
        :height="size.height"
        xmlns="http://www.w3.org/2000/svg"
        baseProfile="full"
        >
        <path :d="svg.path" stroke="#00F23E" stroke-width="3" fill="none"></path>
      </svg>
      <canvas
        v-if="scopeType === 'canvas'"
        ref="scope"
        :width="size.width"
        :height="size.height"></canvas>
  </div>
  <div class="fps-checker">
    <span>{{ fps }}fps</span>
  </div>
</div>

CSS

svg {
  background-color: #333333;
}
canvas {
  background-color: #333333;
}

JavaScript

new Vue({
  el: '#app',
  data() {
    return {
      size: {
        width: 400,
        height: 256
      },
      scopeType: 'svg',
      svg: {
        path: ''
      },
      canvas: {
        ctx: null
      },
      audio: {
        ctx: new AudioContext(),
        osc: null,
        analyser: null,
        running: false
      },
      fps: 0
    }
  },
  methods: {
    start() {
      this.audio.running = true;
      this.audio.analyser = this.audio.ctx.createAnalyser();
      this.audio.analyser.connect(this.audio.ctx.destination);
    
      // 高速フーリエ変換の周波数領域のサイズ
      this.audio.analyser.fftSize = 128;
      
      this.audio.osc = this.audio.ctx.createOscillator();
      this.audio.osc.connect(this.audio.analyser);
      this.audio.osc.start();
      
      this.monitorFPS();
      switch (this.scopeType) {
        case 'svg':
          this.drawSVG();
          break;
        case 'canvas':
          this.drawCanvas();
          break;
      }
    },
    stop() {
      this.audio.osc.stop();
      this.audio.running = false;
    },
    drawSVG() {
      const bufferLength = this.audio.analyser.frequencyBinCount;
      
      const update = () => {
        // 音声波形データをUint8Arrayにコピー
				let data = new Uint8Array(bufferLength);
        this.audio.analyser.getByteTimeDomainData(data);
        
        let d = 'M';
        data.forEach((p, i) => {
        	d += `${i * (this.size.width / bufferLength)} ${p},`;
        });
        this.svg.path = d;
        if (this.audio.running) {
          window.requestAnimationFrame(update.bind(this));
        }
      };
      
      window.requestAnimationFrame(update.bind(this));
    },
    drawCanvas() {
      const ctx = this.$refs.scope.getContext('2d');
      ctx.lineWidth = 3;
      ctx.strokeStyle = '#00F23E';
      
      const bufferLength = this.audio.analyser.frequencyBinCount

      const update = () => {
        ctx.clearRect(0, 0, this.size.width, this.size.height);
        
        ctx.save();
       ...