BLOG

ブログ

新着記事

2020.06.26 Angular

HTMLで使えるオーディオビジュアライザータグを作ってみる

 

はじめまして、Fabeeeブログに初めて投稿するDA2です。

 

現在フロントエンドにAngularを使った映像関連サイトの開発・保守を担当していて、最近ではブラウザのWeb Audio APIを使って再生中の映像の音声データを解析・視覚化したりしています。

 

今回のブログでは、上記のAngular、Web Audio API等の技術を使って簡易なオーディオビジュアライザーを表示するHTMLタグの作り方を紹介しようと思います。

 

具体的には、HTML中に<audio-visualizer src="..."></audio-visualizer>と書けば、といったビジュアライザーが表示されるようになります。

 

1.必要なもの

作業はWindows10を使って進めます。事前に必要なソフトウェアは以下2つです。

  • Node.js(8.9以降)
  • Chrome等のブラウザ(動作確認用)

 

2.Angularプロジェクトのセットアップ

まずは、Angular CLIというツールを使って、ソースコードのひな型を作ります。

> npm install -g @angular/cli
> ng new audio-visualizer --style=scss --routing=false
> cd audio-visualizer
> ng add @angular/elements
  • ※@angular/elements(Angular Elements)をひな型にさらに追加しています。Angularで作ったコンポーネントをHTMLタグ(正確にはCustom Elements(Web Components))化するのに使います。

インストールされた各種パッケージのバージョンはng versionにて確認できます。ブログ執筆時点では以下のとおりでした。

> ng version
Angular CLI: 9.1.8
...
Angular: 9.1.11
...
  • ※@angular/elementsもAngularに分類されるので、バージョンは9.1.11です。

 

3.オーディオビジュアライザーコンポーネントを作る

ひな型のsrc/appフォルダに、以下3つのファイル(Angularコンポーネント)を追加します。

  • audio-visualizer.component.ts
  • audio-visualizer.component.html
  • audio-visualizer.component.scss

 
音声ファイルを再生しつつ、波形データを抽出・視覚化のために加工しているのが.ts(TypeScriptファイル)で、加工されたデータをもとにHTML上にビジュアライザーを描画するのが.html(HTML)と.scss(スタイルシート)になります。
 
audio-visualizer.component.ts

import {Component, Input, OnDestroy, OnInit} from '@angular/core';

@Component({
  templateUrl: 'audio-visualizer.component.html',
  styleUrls: ['audio-visualizer.component.scss']
})
export class AudioVisualizerComponent implements OnInit, OnDestroy {
  @Input() src: string;

  private SPECTRUM_MAX_VALUE = 4;
  private SPECTRUMS_LENGTH = 64;
  private SPECTRUMS_UPDATE_INTERVAL = 50;
  public spectrums: Array<number>; // L:0~31, R:32~63

  public audio: HTMLMediaElement;
  private context: AudioContext;
  private source: MediaElementAudioSourceNode;
  private splitter: ChannelSplitterNode;
  private analysers: Array<AnalyserNode> = [];
  private merger: ChannelMergerNode;

  private timer: number;

  ngOnInit(): void {
    this.spectrums = new Array<number>(this.SPECTRUMS_LENGTH);

    this.audio = new Audio(this.src);
    this.audio.addEventListener('ended', () => {
      this.stopUpdateSpectrums();
    });

    this.context = new AudioContext();
    this.source = this.context.createMediaElementSource(this.audio);
    this.analysers.push(this.context.createAnalyser());
    this.analysers.push(this.context.createAnalyser());
    this.analysers[0].fftSize = this.analysers[1].fftSize = this.SPECTRUMS_LENGTH;
    this.analysers[0].smoothingTimeConstant =
      this.analysers[1].smoothingTimeConstant = 0.5;
    this.splitter = this.context.createChannelSplitter(2);
    this.merger = this.context.createChannelMerger(2);

    this.source.connect(this.splitter);
    this.splitter.connect(this.analysers[0], 0, 0);
    this.splitter.connect(this.analysers[1], 1, 0);
    this.analysers[0].connect(this.merger, 0, 0);
    this.analysers[1].connect(this.merger, 0, 1);
    this.merger.connect(this.context.destination);
  }

  ngOnDestroy(): void {
    this.stopUpdateSpectrums();
    this.merger.disconnect();
    this.analysers[0].disconnect();
    this.analysers[1].disconnect();
    this.splitter.disconnect();
    this.source.disconnect();
    this.context.close();
  }

  onClick() {
    if (this.audio.paused) {
      this.context.resume();
      this.audio.play().then(() => {
        this.startUpdateSpectrums();
      });
    } else {
      this.context.suspend();
      this.audio.pause();
      this.stopUpdateSpectrums();
    }
  }

  private startUpdateSpectrums(): void {
    this.stopUpdateSpectrums();
    this.timer = setInterval(() => {
      const channelSpectrums = new Uint8Array(this.SPECTRUMS_LENGTH / 2);
      this.analysers[0].getByteFrequencyData(channelSpectrums);
      channelSpectrums.forEach((x, i) =>
        this.spectrums[i] = x * this.SPECTRUM_MAX_VALUE / 255);
      this.analysers[1].getByteFrequencyData(channelSpectrums);
      channelSpectrums.forEach((x, i) =>
        this.spectrums[channelSpectrums.length + i] =
          x * this.SPECTRUM_MAX_VALUE / 255);
    }, this.SPECTRUMS_UPDATE_INTERVAL);
  }

  private stopUpdateSpectrums(): void {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
}
  • ※@Componentに「selector」を書くのが一般的ですが、Componentとタグ名との紐づけは「4.HTMLタグの登録」で行うのでここでは省略しています。

audio-visualizer.component.html

<svg width="100%" height="100%" viewBox="0, 0, 48, 48">
  <circle (click)="onClick()" cx="24" cy="24" r="15"></circle>
  <path *ngIf="audio.paused" d="M21 19 L29 24 L21 29 Z"></path>
  <g *ngIf="!audio.paused">
    <rect x="20" y="19" width="3" height="10"></rect>
    <rect x="25" y="19" width="3" height="10"></rect>
  </g>
  <ng-container *ngFor="let x of spectrums; let i = index">
    <rect x="24" y="40" width="1" [attr.height]="x"
      attr.transform="rotate({{360 * (-audio.currentTime * 7 + i) / 64}}, 24, 24)">
    </rect>
  </ng-container>
</svg>
  • ※.htmlファイルにはHTMLを一切書かず、SVGのみを直接書いてビジュアライザーを描画する実装としました。

audio-visualizer.component.scss

:host {
  display: inline-block;
  width: 72px;
  height: 72px;
}
svg {
  rect, path {
    pointer-events: none;
    fill: #003b72;
  }
  circle {
    fill: transparent;
    stroke: #003b72;
    stroke-width: 0.5;
    &:hover {
      fill: rgba(0, 59, 114, 0.07);
    }
  }
}
  • ※「:host」とはaudio-visualizerタグのこと。標準では72×72ピクセルのサイズで描画します。使用する際に外部からスタイルを上書きすれば、任意のサイズで描画できます(SVGなので、綺麗に拡大・縮小します)。

 

4.HTMLタグの登録

ひな型のsrc/appフォルダのapp.module.tsファイルを以下の内容に変更します。customElements.define(~);の部分で、HTMLタグ名「audio-visualizer」を登録しています。
 
app.module.ts

import {BrowserModule} from '@angular/platform-browser';
import {ApplicationRef, Injector, NgModule} from '@angular/core';
import {AudioVisualizerComponent} from './audio-visualizer.component';
import {createCustomElement} from '@angular/elements';

@NgModule({
  declarations: [
    AudioVisualizerComponent
  ],
  imports: [
    BrowserModule
  ]
})
export class AppModule {
  constructor(private injector: Injector) {
  }
  ngDoBootstrap(app: ApplicationRef) {
    customElements.define('audio-visualizer', createCustomElement(
      AudioVisualizerComponent, { injector: this.injector}));
  }
}
  • ※一般的なAngularプロジェクトでは、AppModuleの@NgModuleに「bootstrap: [AppComponent]」を書いて、AppComponentを起点としてAngularアプリケーションを起動しますが、本サンプルではAppComponentを使わずに、AppModuleのngDoBootstrap()にてCustom Elementsの登録のみを行っています。

 

5.動作確認

ひな型のsrc/assetsフォルダに適当な音声ファイル(例えばtest.mp3)を格納し、src/index.htmlのbodyタグを以下の内容に書き換えます。
 
index.htmlのbodyタグ

<body>
  <audio-visualizer src="assets/test.mp3"></audio-visualizer>
</body>

ng serveコマンドを実行し、ブラウザにてhttp://localhost:4200へアクセスすれば、オーディオビジュアライザーが表示されます。

> ng serve
...
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
: Compiled successfully

クリックにて音声ファイルの再生・停止ができます。

 

6.公開用のJavaScriptファイルを生成する

作ったコンポーネントを実際のWEBページで使う場合は、ng buildコマンドでJavaScriptファイルを生成します。また、コマンドの出力結果は複数の.jsファイルとなるため、必要であれば一つのファイル(例えばaudio-visualizer.js)へまとめます。

> ng build --prod --output-hashing=none
> cd dist/audio-visualizer
> type main-es2015.js polyfills-es2015.js runtime-es2015.js > audio-visualizer.js
  • ※Angular 9.1.11では、audio-visualizer.jsのサイズは175KB程度でした。
  • ※~-es2015.jsというファイル以外にも~-es5.jsというファイルも出力されますが、こちらはECMAScript2015をサポートしていない古いブラウザ向けのコードであり、今回は生成ファイルに含めていません。

 

7.使い方

HTML中に以下のscriptタグを書くと、audio-visualizerタグが使えるようになります。

<script src="audio-visualizer.js" type="module"></script>

「5.動作確認」にも書きましたが、audio-visualizerタグにはsrc=”…”で音声ファイルのパス(またはURL)を指定します。

<audio-visualizer src="path/to/music.mp3"></audio-visualizer>

 

8.最後に

Angular(+Angular Elements)、Web Audio API等の技術を使った簡易なオーディオビジュアライザーHTMLタグの定義方法および使い方について紹介させて頂きました。
 
Angular Elementsを使うことで、AngularをSPAサイトのフロントエンドフレームワークとしてではなく、静的なHTMLサイトや他のフレームワーク(Vue.js、React等)サイトの画面コンポーネントを作ることに使えます。Angularの活用の幅が広がる仕組みなので、Angularに触れたことのある方や興味のある方は使ってみてはいかがでしょうか。
 
AIコンサル/SES/受託開発のご依頼についてはこちら