• 検索結果がありません。

南山大学紀要 アカデミア 理工学編第 21 巻, 5-25, 2021 年 3 月 5 Web Audio API Part 1: 1,a) C++ GUI ( ) GUI Android GUI Java Java Windows macos Android ios Web JavaScript

N/A
N/A
Protected

Academic year: 2021

シェア "南山大学紀要 アカデミア 理工学編第 21 巻, 5-25, 2021 年 3 月 5 Web Audio API Part 1: 1,a) C++ GUI ( ) GUI Android GUI Java Java Windows macos Android ios Web JavaScript"

Copied!
21
0
0

読み込み中.... (全文を見る)

全文

(1)

Web Audio API

を用いた音楽アプリケーションの試作

– Part 1:

タイマ,メトロノーム,可変プレーヤ

後藤 邦夫

1,a)

概要:著者は楽器演奏や歌唱の練習に役立つアプリケーションを試作してきた.C++でGUI (グラフィカ

ルユーザインタフェース)なしで始め,GUIつきAndroid版,そしてGUIつきJava版を作成した.しか

し,一般利用者にはJava実行環境のインストールが難しいので,利用が広まらなかった.そこで,本稿で

は,Windows,macOS,Android,iOSの標準的なWebブラウザで互換なアプリケーションをJavaScript

で作成できるか,試作と評価で明らかにした.試作したアプリケーションは,作成順に1)発表用タイマ

(2017),2)高機能メトロノーム(2018),3)速度/ピッチ可変オーディオプレーヤ(2019),4)打音と連続 音分離(2020),である.4)はPart 2で述べる.開発環境にNode.js,Reactを用いて試作したアプリケー

ションを評価した結果,この方法で標準的なWebブラウザで互換,かつ実用的な性能を持つアプリケー

ションが作成できることがわかった.時間的な正確さはオーディオの現在時刻を用いたスケジューリング

用ライブラリWAAClockで実現できた.

キーワード:Web Audio API,JavaScript,音楽アプリケーション

Design and Implementation of Music Application using Web Audio API

– Part 1: Timer, Metronome, Variable Player –

Kunio Goto

1,a)

Abstract: The author has prototyped applications useful for playing musical instruments and practicing singing. Starting in C++ without GUI (Graphical User Interface), we wrote an Android version with a GUI and a Java version with a GUI. However, it is difficult for general users to install the Java runtime environment, so the user did not spread. Therefore, in this paper, we investigate the possibility to write a compatible application with JavaScript in common web browsers for Windows, macOS, Android, and iOS. It was clarified through the prototype applications and their evaluation. The prototype applications are listed below in the order of creation; 1) Presentation timer (2017), 2) Metronome with advanced features (2018), 3) Variable speed/pitch audio player (2019), 4) Separation of percussive and harmonic sound (2020). Note 4) will be described in Part 2 of this paper. As a result of the evaluation of the prototypes using Node.js and React in the development environment. In this way of development, it turned out that it is possible to create an application with compatible and practical performance on standard web browsers. Temporal accuracy was realized by the scheduling library WAAClock, using the current time of audio.

Keywords: Web Audio API, JavaScript, Music Application

1 南山大学国際教養学部Faculty of Global Liberal Studies, Nanzan University a) [email protected]

(2)

1.

はじめに

著者は楽器演奏や歌唱の練習に役立つアプリケーションを試作してきた.2014年ころの目標の一つは,ステレオ音楽の 打音と連続音への分離[11] であり,C++で,コマンド行で動作するアプリケーションを完成した. しかし,一般利用者にとってはコマンドの利用は困難なので,グラフィカルユーザインタフェース(GUI)を備えるため に,作成済のC++ライブラリを用いてクロスコンパイルしたAndroid版を試作したが,CPUの資源不足に起因する音声 の途切れがあり,満足に動作しなかった.また,iOSでは別にプログラムを書く必要があり手間が大きすぎるので,スマー トフォン版作成を一旦断念した.

そこで,PC用Java版を試作し,Linux,Windows,macOSで快適に動作することを確認した[8].知人に利用を勧めた

が,一般利用者にはJava実行環境のインストールが難しいので,利用が広まらなかった.

一方,互換性がなかったJavaScriptが2008年にES5に標準化され,同じJavaScriptプログラムが,スマートフォンを含

むほとんどのWebブラウザで実行できるようになった[12].また,近年のWebブラウザは,動画と音声の再生機能が組み

込まれていて,Web Audio API [14]から,それらの機能を簡単に利用できることも,音声アプリケーションをJavaScript

で書くことの大きな利点である. そこで本稿では,打音と連続音の分離を目標として,それに至るまでのJavaScriptプログラミングにおける落とし穴と 工夫を解説し,それらの機能と性能を評価する. 紹介するアプリケーションは,作成順に1)発表用タイマ (2017),2)高機能メトロノーム(2018),3)速度/ピッチ可変 オーディオプレーヤ(2019),4)打音と連続音分離(2020),である. 本稿では,1)から3)のプログラミングにおける注意点(落とし穴)と工夫を紹介する.4)はフーリエ変換を用いた数学 的な分析を含むアプリケーションで長い説明が必要である.したがって,4)は本稿に続くPart2で紹介する. 2,3節で,本研究の前提となるプログラムの開発環境と作成手順を説明する.4,5,6節では,それぞれ,前述の発表用 タイマ,高機能メトロノーム,そして,速度/ピッチ可変オーディオプレーヤのプログラミングの工夫と実験による評価結 果を述べる.本研究で得られた経験と知見が同種のアプリケーション試作者にとって有用であると期待する. 実行可能プログラムは[8],ソースコードは[7]に置く.

2.

開発環境

本節では,プログラム開発環境を説明する.プログラミングには,主にWindows用PC (メモリ16GB,CPU 2コアIntel

Core i7-7500U 2.7GHz)をUbuntu 18.04LTS (Linux)で使用した.使用ソフトウェアと動作確認機器は次の通りである.

2.1 使用ソフトウェア 本稿執筆時点で各プログラムを更新したときのNode.jsとReactのバージョンは以下の通りである.過去のプログラム 作成時のソフトウェアバージョンは古いが,互換性に問題はない. • Node.js (v12.18.3) [15] • React (16.14.0) [5] • github.com (バージョン管理と公開用Webサーバ) テキストエディタ(任意,ただしUTF-8で保存)

Node.jsはWebブラウザなしで,JavaScriptプログラムを実行する環境であり,パッケージマネージャ(npm)などの開

発ツールを含む.Reactは,Facebook Inc. とコミュニティによるJavaScriptでクライアントプログラムのユーザインタ

フェース構築のためのライブラリである.どちらもオープンソースプロジェクトである.

Node.js内でのReactの利用方法は簡単である.初回は,Node.jsのパッケージマネージャを使いcreate-react-appモ

ジュールをインストールする.create-react-appで新しいプロジェクトを作成すると,同名のディレクトリ内に,アプリケー

ションの雛形が作成され,必要なReactのパッケージがダウンロードされる.図(リスト)1にコマンドの使用例を示す.

1 npm i n s t a l l −g c r e a t e −react −app 2 npx c r e a t e−react −app projectName

(3)

create-react-appの新バージョンではserviceWorkerが必要な場合は,オプション “--template cra-template-pwa”を追 加する.

Reactはクライアント向けのユーザインタフェースに特化しているが,Node.jsはサーバ側のコードも記述できる.しか

し,本稿ではサーバ用の機能は利用しない.また,Webブラウザの機能のみを使うReactの拡張としてAndroidとiOSで

機器の固有機能を使うReact Native [6]があるが,本研究では,2つの理由からReact Nativeは使用しない.1つ目は機

器固有の機能は使わないこと,2つ目は,Android,iOSそれぞれの開発キット,すなわちAndroid StudioとXCodeが必

要なことである.

テキストエディタはvim,Windowsメモ帳,macOSテキストエディット等,何でもよい.Windows/macOS/Linuxで

動作する無料のMicrosoft Visual Studio Code*1 の使用が一般的である.

英数記号(ASCII)以外の日本語文字などを含むファイルを保存するさいには,文字エンコードをReactに合わせてUTF-8

に指定する必要がある.各行末のセミコロンは,後処理で自動挿入されるので,必要ないが, 本稿のコード引用例はコロ

ンありで示す.ただし,クラスまたはメソッド/関数を閉じる}の後には,セミコロンをつけない.

2.2 動作確認機器

筆者が所有する動作確認機器は以下の通りである.

1 Devices used for tests

Device OS Mem(GB) CPU Type

Thinkpad X1 Carbon (2018) Windows 10/Ubuntu 18.04LTS 16 2 Core Intel i7-7500U at 2.5GHz

macMini (2012) Mac OS X 10.15.6 4 2 core Intel i5 at 2.5GHz

SO-02H (2015) Android 7.0 2 4 core Qualcomm Snapdragon 810 at 1.5GHz

iPod Touch (6th gen, 2015) iOS 12.4.8 1 2 core Apple A8 at 1.1GHz

iPhone 6 (2014) iOS 12.4.8 1 2 core Apple A8 at 1.1GHz

動作確認にはOS標準添付のWebブラウザの他にPC用Chromeを使用した.OS添付のWebブラウザとは,Linux

Firefox,MS Edge (最新版はChromium版),macOS Safari,Android mobile Chrome,iOS Safariである.著者が所有 しないiPad (特に最新のiPad OS),iPhone 6にインストールできない最新iOSでの動作確認は知人に依頼した.なお,

Web Audio APIはInternet Explorerでは使えないので,Internet Explorerは動作確認対象外である.

原則として同じプログラムが上記すべての機器で実行できるが,開発過程において,プログラムの欠陥による不具合が多

く発生したのはiOS (iPhone,iPod Touch)であった.iOSには,落とし穴と言えるいくつかの特徴(癖)がある.これに

ついては7節にまとめる.

3.

プログラム作成手順

具体例の前に,create-react-appで自動生成されるファイル配置と変更すべきファイルを説明する.自動生成されるファ イルは合計約300MBと大きいが,ビルド結果は550KB (サンプルの場合)と小さい.図2に主要なファイルの配置と変更 すべき点を示す.node module内のモジュールと使わないファイルは省略する. なお,本稿執筆中にcreate-react-appで生成されるサンプルファイルの構成と内容が大きく変更された.したがって以下 の説明は最新のcreate-react-appの実行後の状態と異なるが,旧スタイルと互換性があるので,本稿の例は変更なしで実行 できる.新スタイルでは,serviceWorker.jsは自動インストールされない. 図の上から簡単に説明する.package.jsonでは,アプリケーション名を変更,実行可能形式での公開予定URLの記述 (”homepage”)を追加する.

favicon.icoは,Reactのロゴタイプなので,自分用のものに入れ替える.index.htmlでは,titleタグ内を自作アプリケー

ション名に変更する.manifest.jsonでは,アプリケーション名と不要なイメージファイルの読み込みを削除する.

プログラムはApp.jsに記述する.もちろん,使用する関数やクラスを別ファイルに記述してもよい.App.cssにはApp.js

で使うスタイルを記述する.

3.1 プログラムの基本構造

最初に実行されるindex.jsを図(リスト) 3に示す.

(4)

 

├── node_modules

│ ├── (omitted)

├── package.json // add "homepage": "URL",

├── public

│ ├── favicon.ico // replace with own favicon

│ ├── index.html // modify title

│ ├── manifest.json // modify

│ └── robots.txt

└── src

├── App.css // CSS for App.js

├── App.js // main program

├── index.css

├── index.js // edit last line to enable offline use

└── serviceWorker.js

 

2 Main files created by create-react-app

1 //// i n d e x . j s 変更不要

2 i m p o r t React from ’ r e a c t ’ ;

3 i m p o r t ReactDOM from ’ r e a c t−dom ’ ; 4 i m p o r t ’ . / i n d e x . c s s ’ ;

5 i m p o r t App from ’ . / App ’ ; // App .の読み込みj s 拡張子( .は省略可j s ) 6 i m p o r t ∗ as serviceWorker from ’ . / serviceWorker ’ ;

7 8 ReactDOM . r e n d e r ( 9 <React . S t r i c t M o d e > 10 <App /> 11 </React . S t r i c t M o d e >, 12 document . g e t E l e m e n t B y I d ( ’ r o o t ’ ) 13 ) ; 14 s e r v i c e W o r k e r . u n r e g i s t e r ( ) ; // r e g i s t e r ( ) でオフライン実行可能

3 (Listing) Installed index.js as the entry point

10行目の “<App />”が,ReactコンポーネントAppを16行目の第2引数のroot (画面id)に表示する命令であり,

Appは次に説明するReactコンポーネント(App.js)である.9から11行目はHTMLに似ているが,JavaScriptの拡張で

あるJSX*2 で記述されている.HTMLタグと区別するためにコンポーネント名は先頭を大文字とする.StrictModeは開

発モード実行において,コードの検査や警告を詳しくする機能である.

index.jsは,コンポーネント名がAppでよいなら変更は不要である.また,試しに“<App />”を複数行書くと,複数の

コンポーネント(アプリケーション)が表示されることを確認できる.オフライン利用を許す場合は,14行目(最終行)の

unregister()をregister()に変更する.Service Worker[13]は,キャッシュつきプロキシサーバの機能を提供し,JavaScript

アプリケーションのオフライン実行を可能にする.

次に,図(リスト) 4 のApp.jsの例を説明する.現在推奨される記述スタイルはES6*3 で導入されたclassを用いた

React.Compornentの子クラス定義である.現在ES6は一部のWebブラウザでしか動作しないので,実行時にはBabel[1]

で互換性があるJavaScriptコードに変換される.

最低限必要なメソッドはrender()である.render()のreturn()内はJSXで記述し変数または関数名が使用できる.この

例では,15行目で表示する文字列を変数で定義した.また,16行目から17行目のbuttonの表示を切り替える記述の例で

ある.なお,return()以外の処理がなければreturn()を省略できる.

この例では与えられたproperty (constructorの引数props)は使わないが,必要があれば,this.props.propnameで利用 *2 https://facebook.github.io/jsx/

(5)

1 //// App . j s

2 i m p o r t React , { Component } from ’ react ’ ; 3 i m p o r t ’ . / App . c s s ’ ; 4 5 c l a s s App e x t e n d s Component { 6 c o n s t r u c t o r ( p r o p s ){ 7 s u p e r ( ) ; 8 t h i s . s t a t e = { buttonStr : ” S t a r t ” } ; 9 t h i s . h a n d l e B u t t o n = t h i s . h a n d l e B u t t o n . b i n d ( t h i s ) ; 10 } 11 12 r e n d e r ( ){ 13 c o n s t S t r i n g = ’ H e l l o ’ ; 14 r e t u r n ( 15 <d i v c l a s s N a m e=”App”><h r />{ S t r i n g}<br /> 16 <b u t t o n name=” s t a r t S t o p ” v a l u e ={ t h i s . s t a t e . buttonStr } 17 o n C l i c k ={ t h i s . handleButton}>{ t h i s . s t a t e . buttonStr}</button> 18 <h r /> 19 </d i v > 20 ) ; 21 } 22 23 h a n d l e B u t t o n ( e v e n t ){ 24 i f ( e v e n t . t a r g e t . v a l u e === ’ S t a r t ’ ) t h i s . s e t S t a t e ({ buttonStr : ’ Stop ’ } ) ; 25 e l s e i f ( e v e n t . t a r g e t . v a l u e === ’ Stop ’ ) t h i s . s e t S t a t e ({ buttonStr : ’ Start ’ } ) ;

26 }

27 }

28 e x p o r t d e f a u l t App ;

4 (Listing) App.js example

できる.

15行目のdivでは使用スタイルのクラス(App.cssで定義)を指定するが,JSXでは,JavaScriptのclassnameをJSX

では,classNameといわゆるcamel caseに変更する.“this.state.buttonStr” はconstructorで宣言された状態変数で,こ

の状態変数を変更すると,自動的にrender()で変更箇所が再描画される.17行目では,onclickをcamel caseで書き,

onClick=this.handleButtonでクリックしたときの処理メソッドを指定する.

23から26行目のhandleButton()がその処理内容である.引数のeventのメンバtarget.valueを用いて場合分けや代入

が可能となる.状態変数は代入ではなく特別に用意されたsetState()で変更する.setState()によりrender()が実行され,

再描画される.

筆者は,当初,以下の間違いに悩んだ.thisのスコープは他のオブジェクト指向言語でのオブジェクトにおける扱いと異

なる.

• setState()はすぐ戻るが,状態変数の変更が済んでいないので,すぐに変更が済んだはずの状態変数を読むとプログラ ムが期待通りに動作しない.

自分で作成したメソッドではthisを参照できない.constructor() でbind(this)を済ませておくか,関数の最後

に.bind(this)を追加する.

開発モードではconstructo()が複数回実行される.static (クラス)変数でオブジェクト数をカウントして気づいた.

図(リスト) 5は簡単なApp.cssの例である.表示幅をスマートフォン用に375 pixelsとして,確認のための表示枠を表 示した.cursorをpointerにする指定は,この例では必要ないが,iOSでクリック(タップ)が認識されない場合に追加す るとよいと言われている

(6)

1 \\ App . c s s 2 . App { 3 t e x t−a l i g n : l e f t ; 4 max−width : 375px ; 5 margin : 0 a u t o ; 6 b o r d e r : 1 px s o l i d #832420; 7 p a d d i n g : 30 px , 8 c u r s o r : p o i n t e r 9 }

5 (Listing) App.css example

3.2 実行とデバッグ

開発モードでの実行は“npm start”だけで簡単である.ローカルWebサーバがTCP port 3000で起動し,ディフォル

ト指定したWebブラウザで,作成したプログラムのページが表示される.デバッグには以下の情報を利用する.

構文エラーはnpm startを実行したターミナルに表示される.そのプロセスを動かしたままテキストエディタで編集

すると,変更がすぐに反映される.

実行中のエラーは,ターミナルとWebブラウザに表示される.

実行中の警告とconsole.log()の出力はWebブラウザのコンソールに表示される.

• JavaScript VM,service-workerの使用メモリ量はWebブラウザの開発ツールのメモリ表示

• Webブラウザタブ(アプリケーション)のメモリ使用量はタスクマネージャで確認する.

Android,iOS装置はコンソール機能がないか,あっても使いにくいので,USBケーブルでPCと接続して,PCのWeb

ブラウザで表示する.他にPCブラウザにReact用の開発ツールadd-onが用意されている.

完成したプログラムは,“npm run build”でパッケージにまとめ,ディレクトリbuild/をpackage.jsonの“homepage”

で指定したURLのWebページにコピーする.例えば,“https://username.github.io/demo”であれば,ディレクトリ名は

buildからdemoに変更する.

次節で,本論のWeb Audio APIを使用するアプリケーション作成手順を述べる.

4.

発表用タイマ

図6に示す発表用タイマを試作した.機能は,減算タイマに加え,発表における予鈴,本鈴,質疑応答時間終了の音声

による通知である.defaultは,予鈴(1分前),本鈴(5分),質疑応答終了(5 + 3 = 8分)である.用意したmp3ファイル

の音声を指定時刻に再生する.”Start”ボタンは,Start,Pause,Continueを兼ねており,ボタン表示文字列がその順に遷

移する.”Reset”ボタンで初期状態に戻す.

JavaScriptソースコードはApp.js (400行),補助関数buffer-loader.js (50行)の合計450行である.npm run buildで

生成した実行可能パッケージは480KBの音声サンプルを含め1MBである.

以下にプログラムの概要を説明する.

4.1 AudioContext

図(リスト) 7にAudioContextの基本的な使用方法を示す.再生を伴わない音声の高速処理にはOfflineAudioContext

を用いる.後者は,part2の打音と連続音の分離アプリケーションで使用する.

Web Audio APIを使用するさいには,まずAudioContextのインスタンスをcomponentDidMount () で生成する.

contextとclockは通常1つしか必要ないのでグローバル変数とした.

Webブラウザ間の互換性のために,AudioContextが使えない場合は,旧版のwebkitAudioContextを使用する記述で

ある.

さらに,後述のAudioContextを使ったスケジューリング用ライブラリのWAAClockのインスタンスもここで生成する.

WAAClock[17]は,blog記事[19]の実装である.

4.2 音声ファイルの読み込みと再生

(7)

6 Presentation timer

1 // o m i t t e d

2 i m p o r t WAAClock from ’ w a a c l o c k ’ ; 3

4 window . AudioContext = window . AudioContext | | window . webkitAudioContext 5 v a r c o n t e x t 6 v a r c l o c k 7 8 c l a s s App e x t e n d s Component { 9 // c o n s t r u c t o r 略 10 componentDidMount ( ) { // a f t e r f i r s t render ( ) 11 c o n t e x t = new window . AudioContext ( ) ;

12 c l o c k = new WAAClock( c o n t e x t ) ;

13 }

14 }

7 (Listing) Creation of AudioContext and WAAClock instances

で簡単にローカルファイルが読み込めるが,ここでは,App.jsから直接読み込む必要がある.セキュリティ上の制約

(CORS*4)から,URLfile://の音声ファイルは読めない.

以下のコード断片は音声ファイルを読み込み再生に適する形式に変換する手順である.これは説明のための例で,クラ

スAppのコードとは異なる.

ローカルファイルをGETするHTTPリクエストを出し,arraybufferとして応答を受け取り,AudioContextの機能で

decodeして,オーディオバッファに格納する.オーディオバッファの内容はサンプリングレートなどの音声データのパラ

メータ情報とリニアPCMデータである.

React実行環境では,読み込みたい音声ファイルをApp.jsと同じディレクトリsrcかそのサブディレクトリに置く必要

がある.簡単には読めず,試行錯誤の結果,対象ファイルをimportすると読み込めた.

console.log()で確認したhotelの値は,

”/demos/presentimer/20201022/static/media/hotel.ed7abcba.mp3”で,package.jsonのhomepageの値 (/demos/presen-timer/20201022/)で始まるbuild時のパス名となっていて,originのWebサーバからのGETとなる.最後のed7a... は ファイル名の衝突を防ぐためのハッシュ値である.

図(リスト) 9に用意したオーディオバッファの再生手順を示す.

createBufferSource()で作成したAudioBufferSourceNodeのインスタンスのbufferに用意したAudioBufferの参照を渡

し,出力先を意味するcontext.destinationに連結後,start()する.引数なしのstart()は,即時再生を意味する.source

(8)

1 i m p o r t h o t e l from ’ . / h o t e l . mp3 ’ ; // h o t e l b e l l s o u n d f o n t 2 3 // c o n t e x t g e n e r a t i o n o m i t t e d 4 l e t sound ; 5 l e t r e q u e s t = new XMLHttpRequest ( ) 6 r e q u e s t . open ( ’GET’ , h o t e l , t r u e ) 7 r e q u e s t . r e s p o n s e T y p e = ’ a r r a y b u f f e r ’

8 r e q u e s t . o n l o a d = f u n c t i o n ( ) { // on loaded the data 9 c o n t e x t . decodeAudioData ( r e q u e s t . r e s p o n s e ,

10 f u n c t i o n ( b u f f e r ) { sound = b u f f e r } , f u n c t i o n ( e r r o r ) // omitted

11 )

12 }

13 r e q u e s t . s e n d ( ) // r e q u e s t i s s e n t h e r e

8 (Listing) Loading a sound a file

1 l e t s o u r c e = c o n t e x t . c r e a t e B u f f e r S o u r c e ( ) 2 s o u r c e . b u f f e r = sound

3 s o u r c e . c o n n e c t ( c o n t e x t . d e s t i n a t i o n )

4 s o u r c e . s t a r t ( ) // s o u r c e . s t a r t ( c o n t e x t . c u r r e n t T i m e , 0 , )

9 (Listing) AudioBuffer playback

は使い捨てで,仕様では再生完了後に使用メモリが開放される.

一般には,start()は3つの引数開始時刻(when),先頭からの再生位置(offset),再生時間(duration)をとる.開始時刻は

現在時刻context.currentTimeを起点として,context.currentTime + 3.0(sec)のように指定する.currentTimeは読み取

り専用プロパティで,double精度のcontext生成時点からの秒数である.

また,context.destinationの前に,音量調整(gain),カスタム処理スクリプトなどを挿入することができる.ループ再 生,再生レートの変更も指定できる.それらの機能は別の例で説明する.

4.3 オーディオタイマを用いたスケジューリング

このアプリケーションで必要なタイマ機能は,1秒ごとの時間表示の変更である.JavaScriptのsetInterval (処理関数,

ミリ秒)で実現できるが,秒の間にPauseする場合は,clearInterval()で予定を取り消し,Continueのさいに,残り時間

の待ち時間をsetTimeout()で指定してから,1秒ごとの繰り返しを再スタートする処理が必要となる.また,JavaScript

のクロックは正確でないことが知られている.

一方,AudioContextのインスタンス(context)のcurrentTimeはオーディオハードウェアのサンプリングレートの精度

を持ち,生成時刻を0とした経過時間を提供する.context.suspend()で一時停止とリソース開放,resume()で再開できる.

AudioContextだけでは,定期繰り返しの記述が難しいが,WAAClockを使用すれば,簡単に記述できる.WAAClock

は,currentTimeに基づき,繰り返し機能や予定登録を実現するライブラリである.理論的背景はWAAClockの作者が参

考にしたBlog記事である.

以上の理由から,正確な発表用タイマの実現のために,WAAClockを使用した.そのコード断片を図(リスト) 10に示す.

1行目は,事前に作成したAudioContext (context)が停止状態(suspended)になっている場合に,再開する手続きであ

る.Google Chromeの新しいバージョンでの仕様の変更で,利用者の操作まで,自動的に再生されない.iOSでも,仕様 に明記されていないが,経験上利用者の操作で最初の再生をスタートする必要がある.このアプリケーションでは,その必

要がなかったが,必要があれば,Start時に短い無音データを再生するコードを挿入すればよい.

Suspendの処理はcontext.suspend()と状態変数startButtonStrの ’Continue’ への変更,同様にContinueの処理は

context.resume()とその状態変数の’Pause’への変更だけである.

4.4 実行結果

(9)

1 i f ( c o n t e x t . s t a t e === ’ s u s p e n d e d ’ ) c o n t e x t . resume ( ) 2 t h i s . s e t S t a t e ({ t i m e r S t y l e : { c o l o r : ’ blue ’ } } )

3 t h i s . params . beginTime = c o n t e x t . c u r r e n t T i m e

4 t h i s . t i m e r E v e n t = c l o c k . c a l l b a c k A t T i m e ( f u n c t i o n ( e v e n t ) { 5 t h i s . p r o c e s s T i m e r ( ) // 残り時間変更,音声再生

6 } . bind ( t h i s ) , context . currentTime ) // すぐ実行

7 . r e p e a t ( 1 . 0 ) // 繰り返し間隔 ( s e c )

8 . t o l e r a n c e ({ e a r l y : 0 . 1 , l a t e : 0 . 1 } ) // 予定とのずれの許容範囲

9 t h i s . s e t S t a t e ({ s t a r t B u t t o n S t r : ’ Pause ’ } ) // 表示変更

10 (Listing) Periodical execution using WAAClock 示す.

2 Timer accuracy

Start 4 min 5 min 8 min

Time 0 −0.04sec −0.05 sec −0.05 sec

時間は最初の4分間で40ミリ秒予定より早く,その後は変化が少なく8分間で50ミリ秒予定より早くなった.発表用 タイマでは計測時間が数分以上であり,数分あたり50ミリ秒の誤差は許容範囲である.スタートボタンを押してから,そ のイベントが表示に反映されるまでに時間がかかり,初回表示が遅れることが原因であると思われる.この問題は,定期的 に音声が再生される5節のメトロノームで調査する.また,PCとスマートフォンでの実行における時間精度もメトロノー ムで比較する. 次に,メモリ使用量を計測した.Google Choromeのタスクマネージャでアプリケーションのタブ,開発ツールの「メモ リ」で表示したJavaScript VMとService-workerの使用メモリ量をそれぞれ,表3に示す.音声サンプルはmp3ファイ

3 Timer memory usage

State TAB(MB) VM(MB) Service-worker(MB)

Initial 38 7.0 1.7 Running 40 7.7 1.7 ルで合計480KBであり,リニア16ビット44.1kHzのWaveファイルに換算すると合計3.9MBである.プログラム中でリ ニアPCM Float32にデコードして使用するので,TABの使用メモリのうち約8MBを音声サンプルが占める.40MBのメ モリ使用は問題なく,時間経過に伴う顕著な増加傾向はない.しかし,Chromeでは,停止後に参照が切れたオブジェクト のメモリがガベージコレクションで解放されず,再開するとTABのメモリ使用が増加するが,数回繰り返しても80MB程 度で動作を妨げるほどではない.Firefoxではこのメモリリークは見られなかった.

5.

メトロノーム

次に高機能メトロノームを試作した.高機能メトロノームはスマートフォンのAppに多く見られるが,PC用のものは 意外と少ない.メインプログラム(App.js)は1500行と少し長くなったが,音源ファイル,リズムパターン(JSONテキス

トファイル)を含む実行用パッケージは1.5MBとAndroidやiOSのAppと比較してコンパクトである.

図11に試作した高機能メトロノームの操作画面を示す. 左は拡張機能を隠した状態の操作画面で,右が拡張機能部分だけの操作画面である.この画面は英語表示であるが,Web ブラウザで設定された言語環境が日本語であれば,操作項目が日本語で表示される.また,’jp’ 以外の言語環境であって も,”JP”ボタンを押すと日本語表示に切り替えられる. 5.1 機能説明 図11左に示す基本機能は以下の通りである. • metro: クリック音– 32種の拍子パターンから選択(4/4, 5/4, 7/4等の拍子,swing) – sound: 20種の音色から選択(default: 3つの音程が異なるカウベル)

(10)

11 Metronome (left) and its advanced features (right)

• dr: ドラムのリズムパターン– 262種の1または2小節パターンから選択

• vo: クリック音またはドラム音に加えて,one, twoの声

• BPM:テンポ調整(beat per minutes) –スライダ,+-ボタン,tap,セレクタで0.1単位の微調整

• Timer: 時間または小節数で指定する練習用タイマ

• Vol: 再生音量調整(装置の音量調整ではない)

クリック音とドラム音は50の短いサンプル音源ファイル(mp3形式)の再生で実現した.ドラム音再生はメトロノーム

よりリズムマシンにある機能であるが,ドラマーのためのメトロノームを試作する目的で本メトロノームに組み込んだ.音

声や練習タイマなどの時間,スケジュールはすべてWeb Audio APIとWAAClockで実現した.ドラム音は,複数の音声

ファイルを同時に再生するので,ずれがないように高い時間精度が必要である.

図11右に示す応用機能は以下の通りである.

• Advanced Control:

– Swing Adj: ハネ具合の調整– 1.5がstraight,2が3連中抜きの普通のswing

– Auto up/down: テンポを指定小節数ごとに上げる/下げる–主にドラマーの練習用

– Random mute: 与えた確率に従い指定した無音小節を入れる–主にドラマーのタイムキープ練習用

– Even notes vol: 0,2,4拍目(down beat)の音を小さくする–主にドラマーのトレーニング用

• SetLists:, SongList: 演奏会や練習の曲順,各曲の拍子とテンポを記録

• Custom Loop: 異なる拍子の小節をつないでパターンを作る–変拍子小節を含む曲の練習用

これらの機能のうち,特に再生スケジューリングに工夫が必要なものは,Swing Adj.とAuto up/downであり,5.3節

でその工夫を詳しく述べる.

SetLists と SongLists は 永 続 性 の た め に ,JSON テ キ ス ト に 変 換 し て ,Webブ ラ ウ ザ の デ ィ ス ク キ ャ ッ シ ュ に 保 存 し ,次 回 の 実 行 で 読 み 込 む .保 存 は ,localStorage.setItem(’Name’, JSON.stringify(Obj)),読 出 し は ,ret = JSON.parse(localStorage.getItem(’Name’)) の手順である.しかし,当然Webブラウザのディスクキャッシュを消去 すると消える.他の機器でも使用できるようにするためには,ローカルファイルに書き出す必要があるが,JavaScriptで はセキュリティ上の制約でローカルファイルには直接書き出せない.幸い直接書き出さずに,ローカルサーバから,Web ブラウザがダウンロード保存する方法がある.その方法は6節のテンポ/ピッチ可変プレーヤで述べる. 5.2 リズムパターンの表現 再生スケジューリングの前にリズムパターンの表現,すなわち楽譜に相当するデータの形式を説明する.リズムパター

(11)

ンは,ドラム教則本*5 5線譜を定義したJSONテキストに書き込んだものである.クリック,ドラムのリズムパターン

の例を1つずつ図(リスト) 12に示す.

1 // c l i c k P a t t e r n s . j s o n

2 [

3 // o m i t t e d

4 {”name ” : ”8/8 swing ” , ” type ” : ” c l i c k s ” , ” numerator ” : 8 ,

5 ” d e n o m i n a t o r ” : 8 , ” s w i n g V a l ” : 2 . 0 , 6 ” p a t t e r n ” : [{ ” note ” : ” c l i c k 0 ” , ” v a l u e s ” : [ 9 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] } , 7 {” note ” : ” c l i c k 1 ” , ” v a l u e s ” : [ 0 , 0 , 7 , 0 , 7 , 0 , 7 , 0 ] } , 8 {” note ” : ” c l i c k 2 ” , ” v a l u e s ” : [ 0 , 7 , 0 , 7 , 0 , 7 , 0 , 7 ] } , 9 {” note ” : ” v o i c e ” , ” v a l u e s ” : [ 1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 ]} ] 10 } , 11 // o m i t t e d 12 ] 13 /// d r u m P a t t e r n s . j s o n 14 [

15 { ”name ” : ”pop001 ” , ” type ” : ” drumkit ” , ” d e f a u l t ” : true ,

16 ” n u m e r a t o r ” : 8 , ” d e n o m i n a t o r ” : 8 , 17 ” p a t t e r n ” : [ 18 {” note ” : ” h i h a t C l o s e ” , ” v a l u e s ” : [ 3 , 3 , 3 , 3 , 3 , 3 , 3 , 3 ]} , 19 {” note ” : ” snareOpenRim ” ,” v a l u e s ” : [ 0 , 0 , 8 , 0 , 0 , 0 , 8 , 0 ] } , 20 {” note ” : ” bass ” , ” v a l u e s ” : [ 7 , 0 , 0 , 0 , 7 , 0 , 0 , 0 ]} , 21 {” note ” : ” v o i c e ” , ” v a l u e s ” : [ 1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 ]} 22 ]} , 23 // o m i t t e d 24 ]

12 (Listing) Rhythm pattern examples

nameはパターンのセレクタに表示される文字列,typeはクリックかドラムキット(セット),denominator (分母)は全

音の分割数で,8の場合,valuesの一つの数が8分音符を示す.numeratorは分子である.swing/shuffle (跳ねる)場合は,

跳ね具合を指定する.swingValは2n + 1番目と2(n + 1) + 1)番目の音の間隔を3としたときの,2n + 12n + 2番目

の音の間隔である.1.5がstraight,2が3連音の2つめを抜いたフルスイングを示す.パターンのデータでは,swingなし

の場合はswingValを省略するが,再生中にUIで跳ね具合を変更できる.

noteは楽器名を示すが,クリックの場合は,具体的な楽器名の代わりにclick0,1,2の種類を入れ,実行時にセレクタで組

み合わせを選択する.ドラムの場合は,楽器名を入れる.voice以外の各パートのvaluesは,0から9で音量を示す.0は

無音である.voiceパートの数は,UIでvoをオンにした場合のone, two, three, fourの意味である.音声サンプルはeight

まで用意した.男声は筆者の声,女声は入力した英文を発音するWebサイトの無料サービスを使って作成した.ドラム音

は著者のアコースティックドラムで録音した音源と使用ライセンス不要な古いシンセサイザーのサンプル音源である.

5.3 再生スケジューリング

再生開始と停止のUIイベント処理を図(リスト) 13に示す.

2から3行目ではボタンを表示し,その文字列はコンストラクタで定義した状態変数this.state.playing (再生中)がtrue

なら”Stop”,falseなら”Start”とする.そのボタンをクリックしたときにstartStopDrums ()が実行される.最初に4.3で 述べたChromeの仕様変更に伴うAudioContextのresume()処理を入れる.8から12行目のコメントアウトされたコード

は,iOSで音が出ない場合に,UIイベントの直後に短い無音を再生することで音が出るようにする処理であった.Chrome

対策のAudioContextのresume()処理により不要となったが,必要な場合があるかもしれないのでコメントアウトして残

した.ボタン操作等の前の再生を抑制する実装の目的は,iOSでは明記されていないが,Chromeと同様に利用者保護であ

ると思われる.

(12)

1 /// i n r e n d e r ( ) r e t u r n ( )

2 Play : <b u t t o n name=’ s t a r t S t o p ’ o n C l i c k ={startStopDrums}>

3 { p l a y i n g ? ’ Stop ’ : ’ Start ’}</ button>

4 /// 5 s t a r t S t o p D r u m s ( e v e n t ) { 6 i f ( c o n t e x t . s t a t e === ’ s u s p e n d e d ’ ) c o n t e x t . resume ( ) 7 /∗ // Unlock iOS 8 l e t b u f f e r = c o n t e x t . c r e a t e B u f f e r ( 1 , 1 , 2 2 0 5 0 ) ; 9 l e t s o u r c e = c o n t e x t . c r e a t e B u f f e r S o u r c e ( ) ; 10 s o u r c e . b u f f e r = b u f f e r ; 11 s o u r c e . c o n n e c t ( c o n t e x t . d e s t i n a t i o n ) ; 12 s o u r c e . s t a r t ( ) ; 13 // End u n l o c k ∗/

13 (Listing) AudioContext activation

次に,図(リスト) 14に本節の主題である再生スケジュール登録処理のコードを示す. 1 // i n s t a r t S t o p D r u m s ( ) 2 c l o c k = new WAAClock( c o n t e x t ) 3 c l o c k . s t a r t ( ) 4 // o m i t t e d 5 l e t n o t e s P e r M i n = t h i s . s t a t e . bpm 6 ∗ ( t h i s . params . denominator / 4) 7 f o r ( l e t n o t e s = 0 ; n o t e s < t h i s . params . n u m e r a t o r ; n o t e s++) { 8 e v e n t = c l o c k . c a l l b a c k A t T i m e ( 9 f u n c t i o n ( e v e n t ) { 10 t h i s . p l a y P a t t e r n ( e v e n t . d e a d l i n e )} . bind ( t h i s ) , 11 t h i s . n e x t T i c k ( n o t e s ) 12 ) . r e p e a t ( ( t h i s . params . n u m e r a t o r ∗ 6 0 . 0 ) / notesPerMin ) 13 . t o l e r a n c e ({ e a r l y : ea r l y , l a t e : l a t e }) 14 t h i s . t i c k E v e n t s [ n o t e s ] = e v e n t 15 } // end f o r

14 (Listing) Scheduling notes playback

まず,WAAClockのインスタンスclockを生成してstart()する.5行目のnotesPerMinは,1分間の音数である.BPM (beat/minutes)は通常4分音で示す.例えば,120BPMの場合,4分音の長さは0.5秒である.図(リスト) 12のように書 いたパターンの1/denominatorが1音の長さで,numeratorが1小節中の音数である.7から15行目のforループでは,1

小節内の各音(複数の音)の再生メソッド, playPattern(),を指定時刻に実行する予約を登録する.12行目のrepeatは,以

後1小節の時間ごとに繰り返し指定である.toleranceで時間のずれの許容範囲を指定する.lateが小さすぎると,実行が

とばされる.このプログラムでは,それぞれ0.1秒とした.メソッドnextTick()は後で説明する.

戻り値のeventは,実行関数へのポインタとcurrentTimeを基準とした予定時刻 (event.deadline)の情報を持つ.実行

予定のキャンセル時にeventのリストが必要なので,14行目でnumeratorの数のeventを配列tickEventsに保存する.

予定のキャンセルは図(リスト) 15の3から6行目の通り各イベントをclear()するだけである.6行目で,配列tickEvents のサイズを0にして,7行目で練習用タイマをクリアする.11から12行目は,UIでテンポを変更したときの処理である. 図(リスト) 16 nextTick()は,WAAClockのデモプログラムに,再生停止後のスタートを起点とした時刻の再計算,跳 ね(swing)の計算機能を追加した予定時刻計算ルーチンである. 図(リスト) 16の8から10行目がスイング処理である.noteIndexは0からdenominator -1 であり,奇数のときに (swingVal - 1.5)に比例するoffsetを追加する. 最後にdrumkitの再生処理を図(リスト) 17に示す.

(13)

1 /// 2 i f ( e v e n t . t a r g e t . name === ’ s t o p ’ ) { 3 f o r ( l e t n o t e s = 0 ; n o t e s < t h i s . t i c k E v e n t s . l e n g t h ; n o t e s++) { 4 t h i s . t i c k E v e n t s [ n o t e s ] . c l e a r ( ) 5 } 6 t h i s . t i c k E v e n t s . s p l i c e ( 0 , t h i s . t i c k E v e n t s . l e n g t h ) 7 t h i s . h a n d l e T i m e r ({ t a r g e t : {name : ’ clearTimer ’ } } ) 8 r e t u r n 9 } 10 // BPM u p d a t e 11 c l o c k . t i m e S t r e t c h ( c o n t e x t . c u r r e n t T i m e , t h i s . t i c k E v e n t s , 12 t h i s . s t a t e . bpm / newBpm)

15 (Listing) Canceling/stretching schedule

1 n e x t T i c k ( n o t e I n d e x ) {

2 c o n s t n o t e I n t e r v a l = 6 0 . 0 / ( t h i s . s t a t e . bpm ∗ t h i s . params . denominator / 4) 3 c o n s t b a r I n t e r v a l = beatDur ∗ t h i s . params . numerator

4 c o n s t c u r r e n t T i m e = c o n t e x t . c u r r e n t T i m e

5 c o n s t r e l a t i v e T i m e = Math . max ( 0 , c u r r e n t T i m e − t h i s . params . startTime ) 6 v a r c u r r e n t B a r = Math . f l o o r ( r e l a t i v e T i m e / b a r I n t e r v a l ) 7 8 l e t o f f s e t = 0 9 i f ( t h i s . params . s w i n g && ( n o t e I n d e x % 2 ) === 1 ) 10 o f f s e t = ( t h i s . s t a t e . s w i n g V a l − 1 . 5 ) / 1 . 5 ∗ n o t e s I n t e r v a l 11 12 r e t u r n t h i s . params . s t a r t T i m e + o f f s e t + 13 c u r r e n t B a r ∗ b a r I n t e r v a l + n o t e s I n t e r v a l ∗ noteIndex 14 }

16 (Listing) Time adjustment

1 // i n p l a y P a t t e r n 2 i f ( c u r r e n t P a t t e r n . t y p e === ’ drumkit ’ ) { 3 f o r ( l e t i = 0 ; i < c u r r e n t P a t t e r n . p a t t e r n . l e n g t h − 1 ; i ++) { 4 c o n s t c u r r e n t = c u r r e n t P a t t e r n . p a t t e r n [ i ] 5 i f ( c u r r e n t . v a l u e s [ c o u n t ] === 0 ) c o n t i n u e // when muted 6 s o u r c e [ i ] = c o n t e x t . c r e a t e B u f f e r S o u r c e ( ) 7 s o u r c e [ i ] . b u f f e r = sound [ c u r r e n t . n o t e ] 8 s o u r c e [ i ] . c o n n e c t ( gainNode [ i ] ) 9 gainNode [ i ] . c o n n e c t ( c o n t e x t . d e s t i n a t i o n ) 10 gainNode [ i ] . g a i n . v a l u e = 11 m a s t e r ∗ p a r s e I n t ( c u r r e n t . v a l u e s [ count ] ) / 9 . 0 12 s o u r c e [ i ] . s t a r t ( d e a d l i n e ) 13 } 14 } // end drumkit

17 (Listing) Drumkit playback

号で0から(numerator - 1)の値をとる.したがって,current.values[count]が,ある瞬間の楽器(note)ごとの再生音量

(0から9)となる.音量は,楽器別のgainNodeで指定し,UIの音量スライダ値もかけて決定する.音量が0のときは,再

生不要なので,6行目以後の処理を省略する.

(14)

の性能を評価する.

5.4 パフォーマンス

本節では,メモリ使用,時間の正確さに関する実験結果を説明する. 5.4.1 メモリ使用

発表用タイマと同じ方法でChromeのメモリ使用を計測した結果を表4に示す.メトロノームでも発表用タイマと同様

4 Metronome memory usage

State TAB(MB) VM(MB) Service-worker(MB)

Initial 44 6.5 1.8

Playing 48 8.0 1.8

に,Chromeでは,停止,再開の繰り返しでTABの使用メモリが増加する.Ubuntu (Linux)のChrome,Edgeも同様の 傾向を示した. 5.4.2 時間の精度 発音時刻を比較するためにすべてClaveの音色で,4/4拍子で240bpmの4分音,8分音,16分音をそれぞれ300秒再 生して記録した.ほとんどの場合,メトロノームは4分音で用いるので,240bpmの4分音は,高速のジャズ曲でのみ使用 される.また,本アプリケーションの限界を調べるために,さらに8分音,16分音も試した. 図18に10分ちょうどに開始する5分間の8分音の開始時と終了時の波形を示す.

18 First and last bars (240bpm 8th notes, 10 to 15 min)

Ubuntu Firefox (top),Ubuntu Chrome,Windows10 Edge,Windows10 Chrome,

macOS Safari,macOS Chrome,Android Chrome,iOS Safari (bottom)

Ubuntu Firefoxでは,最初の音が遅れるが,他のWebブラウザでは概ね正確に再生が開始される.ただし,UIのスター

トボタンを押してから最初の音が再生されるまでに,0.1秒程度時間がかかるので,曲を再生しながら,メトロノームを合

わせる操作は困難である.この時間は解消できない.

再生終了は,300秒に設定した練習用タイマで決定されるが,4つのWebブラウザでは停止が間に合わず余分な1音が再

生された.また,Androidでは,再生終了が2秒以上遅れた.使用したAndroid携帯電話の処理能力不足と内蔵オーディ

オ装置のクロックの遅れの2つの原因が考えられる.詳細は次の発音(onset)の時系列の分析で明らかにする.

表5に,再生終了時間の誤差を示した.Onsetはaubio*6のコマンドaubioonsetで検出した.図18で性能に問題があっ

たFirefoxとAndroidは,この表5でも,16分音の300秒の再生終了が,それぞれ,0.255秒,2.581秒遅れた.ただし,

4分音のメトロノーム利用であれば,Androidも実用上問題はない.

ドラムリズムパターンは,一般的に180bpmまでの8分音,120bpmまでの16分音で構成されるので,240bpmの8分音

(15)

5 Metronome time accuracy (300 sec each)

Web browser 4 notes/sec 8 notes/sec 16 notes/sec #drops

Ubuntu Firefox −0.001 +0.002 +0.255 0 Ubuntu Chrome +0.001 −0.001 −0.000 1 Windows10 Edge +0.021 +0.001 +0.021 0 Windows10 Chrome +0.001 +0.023 −0.001 0 macOS Safari −0.001 +0.001 −0.000 0 macOS Chrome +0.003 +0.001 +0.000 0 Android Chrome +0.741 +1.698 +2.581 14 iOS Safari +0.004 +0.002 +0.002 0 の再生の正確さの評価が必要である.図19に,その評価の一例を示す.左は,再生開始からの時間のずれ(offset),右は, 8分音の間隔(理論値は0.125秒)を示す.最大で2.5ミリ秒以下のOffsetは演奏には影響がない.大気中で約7mを伝搬 -0.003 -0.002 -0.001 0 0.001 0.002 0.003 0 500 1000 1500 2000 2500 offset (sec)

clicks (8 times /sec)

’03-8th.plot’ using 1:4 0.122 0.123 0.124 0.125 0.126 0.127 0.128 0 500 1000 1500 2000 2500 interval (sec) clicks (8 times/sec) ’03-8th.plot’ using 1:3 0.125

19 Offset (left) and interval (right) for 8 notes/sec on Edge

する時間(約20ミリ秒)のずれがあると熟練した演奏者はずれに気づく.例えば,市販のディジタル伝送式ギターワイヤ

レス機器の遅延(latency)は2ミリ秒より大きい.演奏者が気づきやすいのは,クリックの間隔の変位である.Windows

10 Edgeでの実測値は,0.125± 0.0025の範囲にあり,間隔の変位は最大で5ミリ秒である.

したがって,想定したテンポでのメトロノームとしての利用に関しては,どの装置でも十分な性能があるが,実験に用い たAndroid機器では,高速リズムパターンの再生が不正確になる.また,Ubuntu Firefoxでは,特に最初の発音が遅れる ことがわかった.

6.

速度/ピッチ可変プレーヤ

このアプリケーションのUIを図20に示す.主な機能は読み込んだ音声データの再生速度と音程 (ピッチ)の変更であ

る.想定する利用目的は,歌唱や楽器演奏の練習におけるテンポ,キーの変更,聴き取りのためのスロー再生である.追

加機能は,部分的な繰り返し再生(ABリピート)と変更を加えた音声データのファイル保存である.これらの機能は一般

に,音楽用レコーダとDAW (Digital Audio Workstation) ソフトウェアにある機能ではあるが,PCで簡単に利用できる

無料ソフトウェアは少ない.有料ソフトウェアの中ではAmazing Slow Downer *7 が英語圏では一般的で,名前の通りの

スロー再生の他にピッチ変更機能もある.Windows/macOSの他にAndroid用,iOS用がMicrosoft,Apple,Googleの

Appサイトで販売されている.ただし,機能説明ページの情報では,高速再生機能が見当たらない.

速度とピッチの変更機能は既存のライブラリsoundTouchJS[2]を用いたに過ぎないので,その詳細は省略する.本稿で

はファイルの読み込み,再生コントロール,そしてファイル保存機能の実現を中心に説明する.

音声の再生速度はWeb Audio APIのAudioBufferSourceNode.playbackRateで変更できるが,rateを上げる/下げると

ピッチ(音程)が上がる/下がる.そこでピッチを変えずに再生速度を変更する手法が必要になる

soundTouchJSは,2001年ころOlli Parviainenが作成したC言語ライブラリSoundTouch Audio Processing Library[16]

をSteve ’Cutter’ BladesがJavaScript用に実装したものである.soundTouchはSonor4,REAPERなどのDAWを含む *7 https://www.ronimusic.com/

(16)

20 Variable pitch/speed player

多くのソフトウェアで利用されてきたことから実用的であると言える.soundTouchでは,WSOLA (Waveform Similarity

Overlap-Add)[18]. に似たアルゴリズムを用いている.1993年以後も,いわゆる音楽信号のTSM (Time-Scale Modification)

の研究は続いていて[4]などで近年の進展を見られる.これらの手法を用いれば,再生速度と再生レートの組み合わせで, ピッチも変更できる. 6.1 ファイルの読み込みと書き出し処理 JavaScript標準機能のFileReaderを使ったファイル読み込み手順を図(リスト) 21に示す. 1 1 ) {m. input } : <br /> 2 <span c l a s s N a m e=” s e l e c t F i l e ”>

3 <i n p u t t y p e=” f i l e ” name=” l o a d F i l e ”

4 a c c e p t =’ a u d i o /∗ ’ onChange={l o a d F i l e } /><br /> 5 </span> 6 //// i n l o a d F i l e ( ) 7 // o m i t t e d 8 l e t r e a d e r = new F i l e R e a d e r ( ) 9 r e a d e r . o n l o a d = f u n c t i o n ( e ) { 10 a u d i o C t x . decodeAudioData ( r e a d e r . r e s u l t , 11 f u n c t i o n ( a u d i o B u f f e r ) { 12 t h i s . params . a u d i o B u f f e r = a u d i o B u f f e r 13 // o m i t t e d 14 } . bind ( t h i s ) , 15 f u n c t i o n ( e r r o r ) { 16 // o m i t t e d 17 }) ; 18 } . bind ( t h i s ) 19 r e a d e r . r e a d A s A r r a y B u f f e r ( f i l e )

(17)

HTMLのinputタグtype=”file” を表示すると,Webブラウザの機能でファイルのMIMEタイプをaudioに限定した

ファイル選択画面が表示され,ファイル選択が完了するとメソッドloadFile()が実行される.ファイル読み込み手順は,タ

イマの場合(図(リスト) 8)と同様である.違いは,FileReaderの使用と19行目のデータ形式指定である.読み込めるの

は,ローカルファイルだけであるが,iCloud,Google Driveなど,クラウドストレージ上のファイルもオフライン利用可

能とすれば利用できる.

ファイル書き出し手順を図(リスト) 22に示す.

1 i m p o r t { saveAs } from ’ f i l e −saver ’ ;

2 i m p o r t ∗ as toWav from ’ a u d i o b u f f e r −to−wav ’ ; 3 // o m i t t e d 4 fakeDownload ( a u d i o B u f f e r ){ 5 c o n s t words = t h i s . params . f i l e n a m e . s p l i t ( ’ . ’ ) ; 6 l e t outFileName = words [ 0 ] 7 + ’& s ’ + p a r s e I n t ( t h i s . s t a t e . p l a y S p e e d ) 8 + ’&p ’ + p a r s e I n t ( t h i s . s t a t e . p l a y P i t c h∗100) 9 + ’ . wav ’ ;

10 l e t b l o b = new Blob ( [ toWav ( a u d i o B u f f e r ) ] , { type : ’ audio /vnd . wav ’ } ) ; 11 s a v e A s ( b l o b , outFileName ) ;

12 }

22 (Listing) Exporting audio file

file-saver[9]は,WebブラウザからのGET要求に対し一時的にローカルWebサーバとしてファイルを提供することで

書き出しを実現する.audiobuffer-to-wav[10]は,Float32で表現されたAudioBufferのデータをWaveヘッダを追加した

Wave形式の音声データに変換する.そのデータをMIMEタイプaudio/vnd.wav に指定したblob (binary large object)

とし,最後に11行目にsaveAsファイル名を指定してサーバで提供する.ファイル名は元のファイル名に再生速度,ピッ

チの情報を追加した文字列とする.

なお,iOSでは,blob (binary large object)形式でダウンロードしたWaveファイルは自動再生できず,再生するには, ダウンロード後にファイル名を変更する必要がある.

6.2 カスタム音声処理

カスタム音声処理は,ScriptProcessorNodeで提供されていたが,2014年8月時点で,非推奨となりAudioWorkletに変更

された.ScriptProcessorNodeでの音声加工はメインスレッドの中で実行されるので,計算負荷が高いと,音が途切れたり,

UIの反応が遅くなる.一方AudioWorkletのAudioWorkletNodeは別スレッドで実行されるので,近年の複数コアCPU

の機器ではメインスレッドの処理を妨げない.soundTouchJSのWorklet版[3]が公開されたが,現時点でAudioWorklet

はmacOS Safari,iOS Safariには組み込まれていないので,本プログラムではScriptProcessorNodeを用いた.

どちらの場合も,カスタム音声処理ノード (ScriptProcessorNode) を図(リスト) 17 の GainNodeのように,source

とdestinationの間に挿入して用いる.soundTouchJSの場合,ScriptProcessorNodeはモジュール内で生成され,普通の

source.start()の手順なしに再生が開始され,音声データの最後に達しても終了しないので工夫が必要である.

図(リスト) 23に主要な処理を示す.まず,プログラムの先頭(1行目)でsoundTouchJSのモジュールをimportする.

5行目の再生開始時に,AudioContextのインスタンス(audioCtx),音声データ (audioBuffer),バッファサイズを引数と

して,PitchShifterのインスタンスを生成する.バッファサイズは2のべき乗で指定する.短すぎると音が途切れることが

あること,10行目以後のコールバックの回数を減らすために長めにした.6から7行目で再生テンポとピッチを状態変数

this.state.playSpeed (%)とthis.state.playPitch (半音が1)の値で設定する.平均律の半音の周波数比がe121 である.テ

ンポとピッチは再生中にUIで変更できる.

10から16行目は,PitchShifterが再生中に呼び出されるコールバック関数である.18行目でshifterをgainNodeにつ なぎ,gainNodeをdestinationにつないだときに,音声の加工と再生が始まり,このコールバック関数が実行される.

コールバック関数の処理は次の通りである.11から12行目では,音楽の開始からのその時点での再生位置を計算し,状

(18)

1 i m p o r t { P i t c h S h i f t e r } from ’ soundtouchjs ’ ; 2 // o m i t t e d 3 l e t b u f f e r S i z e = 1 6 3 8 4 ; 4 i f ( s h i f t e r ) { s h i f t e r . d i s c o n n e c t ( ) ; s h i f t e r . o f f ( ) ; s h i f t e r= n u l l ; } 5 s h i f t e r = new P i t c h S h i f t e r ( audioCtx , p a r t i a l A u d i o B u f f e r , b u f f e r S i z e ) 6 s h i f t e r . tempo = t h i s . s t a t e . p l a y S p e e d / 1 0 0 . 0 7 s h i f t e r . p i t c h = Math . pow ( 2 . 0 , t h i s . s t a t e . p l a y P i t c h / 1 2 . 0 ) 8 l e t d u r a t i o n = s h i f t e r . f o r m a t t e d D u r a t i o n ; 9 10 s h i f t e r . on ( ’ p l a y ’ , d e t a i l => { 11 l e t c u r r e n t P o s = p a r s e F l o a t ( timeA ) + p a r s e F l o a t ( d e t a i l . t i m e P l a y e d ) ; 12 t h i s . s e t S t a t e ({ playingAt : currentPos , p l a y i n g A t S l i d e r : currentPos }) ; 13 14 i f ( d e t a i l . f o r m a t t e d T i m e P l a y e d >= d u r a t i o n ) { 15 s h i f t e r . d i s c o n n e c t ( ) ; s h i f t e r . o f f ( ) ; s h i f t e r = n u l l ;} 16 }) ; // end s h i f t e r . on 17 18 s h i f t e r . c o n n e c t ( gainNode ) ; 19 gainNode . c o n n e c t ( a u d i o C t x . d e s t i n a t i o n ) ; // p l a y b a c k s t a r t 図23 (Listing) PitchShifter playingAtSliderスライダで表示される整数に丸めた時間である.これらはsetState()のたびにUIで更新表示される.14

から15行目が終了処理で,shifterの接続をdisconnect()で切り,off()で停止する.これで,コールバックも終了する.

PitchShifterの仕様により,インスタンスは使い捨てにするので,次回の再生では,新しいインスタンスを使う.

次に,図(リスト) 24にScriptProcessorNodeを用いた加工済みの音声データの保存手順を示す.一般には,この方法で なくOfflineAudioContextを用いる.オフライン処理では,音声の再生なしに,連続実行による音声加工の処理結果を得ら れるが,PitchShifterがOfflineAudioContextで機能しないので,追加した処理である.ScriptProcessorNodeの本来の使用

目的は音声データの加工であるが,ここでは保存が目的なので,入力データ(inputBuffer)を無加工で保存用オブジェクト

(exportBuffer)と出力用オブジェクト(outputBuffer)にコピーした.音声出力がないと処理が停止するので,outputBuffer

へのコピーも必要であった.まず,28から30行目でshifterを保存用saverNodeに,saverNodeをgainNodeに,gainNode

をdestinationにつなぐ.shifterの内部でもScriptProcessorNodeを使用するので,2つのScriptProcessorNodeインスタ

ンスを直列に使うことになる.この接続順では,gainNodeが最後なので,再生中の音量調整は無効になる.

1行目でバッファサイズ(PitchShifterと同じ),入力チャネル数,出力チャネル数を引数としてScriptProcessorNode

のインスタンスを生成する.2行目は保存用AudioBufferを再生速度に合わせて生成する.AudioBufferの長さは変更

できない.6から18行目のsaverNode.onaudioprocessはコールバック関数で,その処理単位は指定したbufferSizeで

ある.event.inputBuffer,event.outputBufferは入力データと出力データの参照でチャネルごとの音声サンプル配列を

getChannelData()で参照し,14行目でinputBufferをoutputBufferにコピーする(一般には加工して代入).保存用の

exportBufferには追記する.

20から26行目のshifter.onの終了処理は,図(リスト) 23のコードと同様である.違いは,Webブラウザに保存データ

をダウンロードさせる24行目のfakeDownload()である.

6.3 再生コントロール

AudioContextには,一時停止(suspend())と再開(resume()),単純なループの機能はあるが,巻き戻し,早送り等の機能

がない.また,PitchShifterを用いると,それらの機能も使えない.したがって,元の音声データから再生する区間の分を 別のAudioBufferにコピーしてPitchShifterで再生することで,それらの機能を実現した.具体的には以下の通りである. 一時停止と再開: 停止位置以後のデータを再生 早送り,巻き戻し: 時間スライダー操作で開始位置を指定,開始位置以後のデータを再生 • ABリピート: ボタンで指定したA,B位置の範囲のデータを繰り返し再生, ループ間隔: 指定した長さの無音再生

(19)

1 s a v e r N o d e = a u d i o C t x . c r e a t e S c r i p t P r o c e s s o r ( b u f f e r S i z e , c h a n n e l s , c h a n n e l s ) ; 2 t h i s . params . e x p o r t B u f f e r = a u d i o C t x . c r e a t e B u f f e r ( c h a n n e l s , p a r s e I n t ( a u d i o B u f f e r . l e n g t h∗(100/ t h i s . s t a t e . p l a y S p e e d ) ) + b u f f e r S i z e , a u d i o B u f f e r . s a m p l e R a t e ) ; 3 4 l e t b a s e = 0 ; 5 l e t e x p o r t B u f f e r = t h i s . params . e x p o r t B u f f e r ; 6 s a v e r N o d e . o n a u d i o p r o c e s s = f u n c t i o n ( e v e n t ){ 7 l e t i n p u t B u f f e r = e v e n t . i n p u t B u f f e r ; 8 l e t o u t p u t B u f f e r = e v e n t . o u t p u t B u f f e r ; 9 10 f o r ( l e t c h a n n e l = 0 ; c h a n n e l < i n p u t B u f f e r . numberOfChannels ; c h a n n e l++){ 11 l e t i n p u t D a t a = i n p u t B u f f e r . g e t C h a n n e l D a t a ( c h a n n e l ) ; 12 l e t outputData = o u t p u t B u f f e r . g e t C h a n n e l D a t a ( c h a n n e l ) ; 13 l e t e x p o r t D a t a = e x p o r t B u f f e r . g e t C h a n n e l D a t a ( c h a n n e l ) ; 14 o u t p u t B u f f e r . copyToChannel ( i npu tDa ta , c h a n n e l , 0 ) ; 15 e x p o r t B u f f e r . copyToChannel ( in putD ata , c h a n n e l , b a s e ) ;

16 } // end f o r channel

17 b a s e += i n p u t B u f f e r . l e n g t h ;

18 } . bind ( t h i s ) ; // end o n aud io p ro ces s

19 20 s h i f t e r . on ( ’ p l a y ’ , d e t a i l => { 21 // o m i t t e d 22 i f ( d e t a i l . f o r m a t t e d T i m e P l a y e d >= s h i f t e r . f o r m a t t e d D u r a t i o n ) { 23 s a v e r N o d e . d i s c o n n e c t ( ) ; s h i f t e r . o f f ( ) ; s h i f t e r . d i s c o n n e c t ( ) ; s h i f t e r = n u l l ; 24 t h i s . fakeDownload ( t h i s . params . e x p o r t B u f f e r ) ; 25 } 26 }) ; // end s h i f t e r . on ( ) 27 28 s h i f t e r . c o n n e c t ( s a v e r N o d e ) ; 29 s a v e r N o d e . c o n n e c t ( gainNode ) ; 30 gainNode . c o n n e c t ( a u d i o C t x . d e s t i n a t i o n ) ;

24 (Listing) SaverNode (ScriptProcessorNode)

6.4 パフォーマンス

ソースコードは753行でbuildサイズは656KBである.使用したライブラリ,soundTouchJSは,JavaScriptだけで記

述されたCPUリソースをある程度利用するカスタム音声処理である.幸い,用意したすべてのWebブラウザで問題なく

実行できた.

メモリ使用は,発表用タイマと同じ方法でWindows Chromeの表示で計測した.結果を表6に示す.Linux,macOSの

Chromeでもメモリ使用量はほぼ同じであった.service-workerの2MBは,ダウンロードしたプログラムのキャッシュに

6 Memory usage

Chrome tab(MB) JavaScript VM service-worker

Initial 33 5.3 2.0 Load 136 5.4 2.0 PlayAB 235 7.1 2.0 PlayAll/Save 235 Pause/play 320+ 使用されたメモリで一定である.JavaScript VMも使用中に5.3MBから7.1MBと少し増え,その後増減する.実験ごと に少し値が変わるが,8MBを超えなかったので,表では省略した. 増加が激しいのはWebブラウザタブの使用メモリである.起動時は33MBで,音楽ファイルを読み込む(Load)すると 一気に136MBに増える.この理由は音源データを32ビット浮動小数点(Float32)でメモリ上のAudioBufferに持つから

(20)

である.

この実験では,270秒の44.1kHzステレオMP3音楽ファイルを使用した.MP3形式では4,328,685 bytes (約4MB)し

かないが,Wavファイルにすると51,936,044 bytes (約50MB)になる.さらに,プログラム内部ではFloat32で保持する

ので,読み込んだときに,その2倍のメモリ(約100MB)を使用する. PlayAB (ABリピート)での初回の再生では,さらに100MBメモリ使用が増加する.これはカスタム音声加工ライブラ リ(soundTouchJS)内部バッファの音声データの複製に起因する.PlayAll/Save (すべて再生して保存)も同様である.2 回目のPlayABでは,さらに100MBメモリが消費され,繰り返すごとに約100MB消費する.PlayABでは,もとの音声 データの部分コピー(AからBまで)を処理対象とするが,soundTouchJSの処理が済んだ後にこのコピーが使用するメモ リが解放されなかったことを意味する.この症状はFirefoxでは見られなかったので,原因はChrome固有のガベージコレ クションの仕様かバグであると推定できる.

7.

おわりに

本稿では,Web Audio APIを用いた音楽のための3つのアプリケーションを試作した.開発環境にはNode.js,React

を用いた.動作実験と性能評価の結果,この方法でWindows,macOS,Android,iOSの標準的なWebブラウザで互換か

つ実用的な性能を持つアプリケーションが作成できることがわかった.時間的な正確さは,オーディオの現在時刻を用い たスケジューリング用ライブラリWAAClockで実現できた.本稿で述べたアプリケーションの実行形式とソースコードは それぞれ[7], [8]に置いた. プログラミングの試行錯誤過程において,以下の注意点と必要な工夫が明らかになった. ( 1 ) Reactの状態変数の変更は即時でない–即時変更を期待した処理を書かない ( 2 ) iOS機器の特徴–カーソル表示を変更,メモリ管理がシビア(配列添字バグで致命的な誤作動),blobファイルダウン ロード時にファイル保存操作が難しい ( 3 )クリックで開始しないと音声がでない– iOSと最新Chromeの仕様

( 4 )カスタム音声処理– AudioWorkletが実装されていないWebブラウザがあるので,当面はScriptProcessorNode (2014

年に非推奨)を使う

( 5 )ローカルファイルの読み込みと書き出し(セキュリティ上の制約) –疑似アップロードとダウンロード処理で実現

( 6 )メモリ使用の削減–各Webブラウザで確認

今後の課題は,多くのCPUとメモリが必要なカスタム音声処理のパフォーマンスとAudioWorklet,OfflineAudioContext

の利用方法の詳細である.これらはpart2の「打音と連続音分離 (2020)」で述べる.他に,本研究の対象ではないが,

JavaScriptで作成したアプリケーションの有料販売方法が不明である.バイナリプログラムであれば,利用者によるダウ ンロード保存とライセンスキーの通知で解決できるが,本稿のアプリケーションはキャッシュできるがファイル保存は難 しい.また,ソースプログラムが可読なので,ライセンスキーによる不正使用の抑制が困難である.

表 1 Devices used for tests
図 2 Main files created by create-react-app
図 4 (Listing) App.js example
図 5 (Listing) App.css example
+7

参照

関連したドキュメント

Here we only present and prove an Orlicz norm version of the inequality (1.5) [and of its extension to the power weight case see, e.g., (2.6) with/3 1 + Zp and give an example of

The new, quantitative version 3.3 (i) of the Combi- natorial Nullstellensatz is, for example, used in Section 5, where we apply it to the matrix polynomial – a generalization of

In this work, our main purpose is to establish, via minimax methods, new versions of Rolle's Theorem, providing further sufficient conditions to ensure global

サーバー API 複雑化 iOS&amp;Android 間で複雑な API

(3) We present a JavaScript library 2 , that contains all the al- gorithms described in this paper, and a Web platform, AGORA 3 (Automatic Graph Overlap Removal Algorithms), in

In this diagram, there are the following objects: myFrame of the Frame class, myVal of the Validator class, factory of the VerifierFactory class, out of the PrintStream class,

The group acts on this space by right translation of functions; the implied representation is smooth... We want to compute the cocy-

Bluetooth® Low Energy プロトコルスタック GUI ツールは、Microsoft Visual Studio 2012 でビルドされた C++アプリケーションです。GUI