js-synthesizer
js-synthesizer はサウンドフォントを使ってMIDIシーケンスを波形に変換し、主にWebページ上で再生できるようにしたライブラリです。
[更新 2023/01/02: sample.js が一部のブラウザーで再生できない問題を修正しました。 / libfluidsynth を更新し、.sf3 ファイルを利用できるようにしました。]
サンプル
Status: Stopped
※ サウンドフォントは openwrld 氏(openwrld@kebi.com)の「ChoriumRevA」(Chorium Revision A)を利用しています。
※ サウンドフォントのダウンロードに数十秒かかる可能性があります(サイズ: 27.5MiB)。
インストール
npm パッケージとして配布しています。
npm install --save js-synthesizer
なお、js-synthesizer の実行に必要な libfluidsynth はパッケージに含まれています。ただしライセンスが異なるため、取り扱いにご注意ください。
使い方
※ README.md の内容を日本語で記述したものです。
メインスレッド処理での利用
パッケージから dist/js-synthesizer.js
(または dist/js-synthesizer.min.js
) と externals/libfluidsynth-2.1.9.js
(libfluidsynth のJSファイル) をコピーし、<script>
タグを以下の順番で記述します。
<script src="libfluidsynth-2.1.9.js"></script>
<script src="js-synthesizer.js"></script>
スクリプトが読み込まれたら、初期化待ちとして JSSynth.waitForReady
関数(戻り値が Promise)が Resolve 状態になるまで待機します。
JSSynth.waitForReady().then(loadSynthesizer);
function loadSynthesizer() {
// JSSynth の各種APIが利用可能
}
初期化が完了したら、JSSynth
名前空間経由で各種APIの利用が可能になります。
// AudioContext のインスタンスを作成
var context = new AudioContext();
var synth = new JSSynth.Synthesizer();
synth.init(context.sampleRate);
// 出力用に AudioNode (ScriptProcessorNode) を作成
// ※ AudioWorklet の利用は後述
var node = synth.createAudioNode(context, 8192); // 8192 is the frame count of buffer
node.connect(context.destination);
// SoundFont の読み込み (sfontBuffer は事前に用意した ArrayBuffer のオブジェクト)
synth.loadSFont(sfontBuffer).then(function () {
// SMF ファイルデータの読み込み (smfBuffer は事前に用意した ArrayBuffer のオブジェクト)
return synth.addSMFDataToPlayer(smfBuffer);
}).then(function () {
// 読み込んだ SMF データの再生
return synth.playPlayer();
}).then(function () {
// 再生が完了するまで待機
return synth.waitForPlayerStopped();
}).then(function () {
// 音が停止するまで待機
return synth.waitForVoicesStopped();
}).then(function () {
// Synthesizer の解放(破棄)
synth.close();
}, function (err) {
console.log('Failed:', err);
// Synthesizer の解放(破棄)
synth.close();
});
※ 上記例では Web Audio API を使っていますが、Synthesizer の render
メソッドを使うことで Web Audio 無しでも動作させることができます。
js-synthesizer は Webpack などの各種バンドラーを利用することで CommonJS / ES モジュールとしても読み込み可能です。import
文を使う場合は import * as JSSynth from 'js-synthesizer'
と記述します。
js-synthesizer.js
は ES2015 準拠の環境で動作する前提で作成されています。「サポートしていません」を表示できるようにするなどの目的で、IE11 などの ES2015 非対応の環境を考慮する場合は、スクリプトを動的読み込みするか、Babel などのトランスパイラーを利用するなどの対応が必要になります。- スクリプト読み込み直後の場合は、libfluidsynth の準備が完了していないために各種APIの利用に失敗する可能性があります。準備完了を待つには、上記例にあるように
JSSynth.waitForReady
関数を利用するようにしてください。 - libfluidsynth のJSファイルは
import
文などでの読み込みに対応しておらず、またライセンス(LGPL-2.1)が js-synthesizer のもの(BSD-3-Clause)と異なりますので、取り扱いにご注意ください。
AudioWorkletでの利用
js-synthesizer は dist/js-synthesizer.worklet.js
(または dist/js-synthesizer.worklet.min.js
) を利用した AudioWorklet での再生をサポートしています。実際のコードは以下のようになります。
var context = new AudioContext();
context.audioWorklet.addModule('libfluidsynth-2.1.9.js')
.then(function () {
return context.audioWorklet.addModule('js-synthesizer.worklet.js');
})
.then(function () {
// AudioWorkletNode を利用する Synthesizer オブジェクトの作成
var synth = new JSSynth.AudioWorkletNodeSynthesizer();
synth.init(context.sampleRate);
// ※ Synthesizer のメソッドを利用するために AudioWorkletNode の作成が必要です
// (内部で利用する MessagePort が AudioWorkletNode を作成するまで
// 利用できないのが理由です)
audioNode = synth.createAudioNode(context);
audioNode.connect(context.destination); // or another node...
// AudioWorkletNode 作成後各種 Synthesizer メソッドが利用可能
return synth.loadSFont(sfontBuffer).then(function () {
return synth.addSMFDataToPlayer(smfBuffer);
}).then(function () {
return synth.playPlayer();
}).then(function () {
...
});
});
Web Workerでの利用
js-synthesizer と libfluidsynth はWeb Worker上でも実行可能です。Web Worker上で実行することで、メインスレッドの処理がブロックされるのを防ぐことができます。
Web Workerで利用するには、単純に以下のようにWorker内で importScripts
を呼び出します。
self.importScripts('libfluidsynth-2.1.3.js');
self.importScripts('js-synthesizer.js');
ただし、Web Audio APIは現状Web Worker上で利用できないため、Web Audioに関連するAPI(メソッド)を利用することは出来ません(render
メソッドの結果をメインスレッドなどに転送する必要があります)。
Web WorkerとAudioWorkletの両方を利用する場合は、以下のような役割で別途AudioWorkletProcessorを実装する必要があります。
- メインスレッド -- AudioWorkletNode を作成し、Web WorkerとAudioWorklet間の通信を確立
- Web WorkerからAudioWorkletにオーディオフレームを転送する必要がありますが、AudioWorklet内でWeb Workerを作成することは出来ないため、メインスレッドで
MessageChannel
を生成した上で各portをAudioWorklet・Web Workerそれぞれに転送することで、Web WorkerとAudioWorkletが直接通信できるようになります。
- Web WorkerからAudioWorkletにオーディオフレームを転送する必要がありますが、AudioWorklet内でWeb Workerを作成することは出来ないため、メインスレッドで
- Web Workerスレッド -- (js-synthesizer を使って)オーディオフレームを生成し、AudioWorkletに送信
- AudioWorkletスレッド -- オーディオフレームを受信して(キューなどに貯め)、
process
メソッドで出力
API
※ README.md の内容を日本語で記述したものです。
Synthesizer インスタンスの生成
以下のクラスが Synthesizer インスタンスを示す JSSynth.ISynthesizer
インターフェイスを実装しています。
JSSynth.Synthesizer
(初期化:new JSSynth.Synthesizer()
)- 通常の Synthesizer インスタンスを生成します。コンストラクターに引数はありません。
JSSynth.AudioWorkletNodeSynthesizer
(初期化:new JSSynth.AudioWorkletNodeSynthesizer()
)- AudioWorklet を利用する Synthesizer インスタンスを生成します。コンストラクターに引数はありません。
- 他のメソッドを利用する前に
createAudioNode
メソッドを呼び出して AudioNode を生成する必要があります。
Sequencer インスタンスの生成
Sequencer インスタンスは MIDI イベントなどを動的に生成し時間(タイミング)を指定して Synthesizer に送信するために利用するインスタンスです。以下のメソッドを利用して生成します。
JSSynth.Synthesizer.createSequencer
メソッド (スタティックメソッド)JSSynth.ISequencer
インスタンスで Resolve する Promise オブジェクトが返ります。JSSynth.Synthesizer
インスタンスとのセットで利用できます。
JSSynth.AudioWorkletNodeSynthesizer.prototype.createSequencer
メソッド (インスタンスメソッド)JSSynth.ISequencer
インスタンスで Resolve する Promise オブジェクトが返ります。createSequencer
メソッド呼び出しに利用したJSSynth.AudioWorkletNodeSynthesizer
インスタンスとのセットで利用できます。
MIDI などのイベントデータのハンドル・フック
プレーヤーが送信したMIDIイベントなどを別途定義した関数でハンドル・フックすることが可能です。JSSynth.Synthesizer
インスタンスの場合は、以下のように hookPlayerMIDIEvents
を利用します。
syn.hookPlayerMIDIEvents(function (s, type, event) {
// '0xC0' イベント (Program Change イベント) のフック
if (type === 0xC0) {
// 'program' が 0 なら別の SoundFont を利用する
if (event.getProgram() === 0) {
syn.midiProgramSelect(event.getChannel(), secondSFont, 0, 0);
return true;
}
}
// false を返すことで既定の処理に回す
return false;
});
JSSynth.AudioWorkletNodeSynthesizer
インスタンスの場合は、以下のように hookPlayerMIDIEventsByName
メソッドを使って対応します。
- worklet.js
// AudioWorklet の別モジュールで使用できるようにするため、AudioWorkletGlobalScope に関数を追加する必要があります
AudioWorkletGlobalScope.myHookPlayerEvents = function (s, type, event, data) {
if (type === 0xC0) {
if (event.getProgram() === 0) {
// 'secondSFont' は 'hookPlayerMIDIEventsByName' で渡されたデータ
s.midiProgramSelect(event.getChannel(), data.secondSFont, 0, 0);
return true;
}
}
return false;
};
- main.js
// 以下のコードを実行する前に、上記の 'worklet.js' が AudioWorklet として読み込まれ、
// syn.createAudioNode メソッドを呼び出して AudioWorklet が実行状態になっている必要があります。
// 第1引数は AudioWorkletGlobalScope に追加した関数名
// 第2引数は worklet に渡したいデータ
syn.hookPlayerMIDIEventsByName('myHookPlayerEvents', { secondSFont: secondSFont });
Sequencer ではイベントデータをハンドルするための「ユーザー定義クライアント」をサポートしています。
Synthesizer.createSequencer
で作成した Sequencer では、Synthesizer.registerSequencerClient
スタティックメソッドを使って登録します。Synthesizer.sendEventNow
スタティックメソッドを使うと、他の Synthesizer やクライアントなどで処理されたイベントデータを別のクライアント(またはSynthesizer)に送信することができます。
AudioWorkletNodeSynthesizer
のcreateSequencer
メソッドで作成した Sequencer では、registerSequencerClientByName
インスタンスメソッドを使って登録します。- クライアント用のコールバック関数は AudioWorkletGlobalScope に追加されている必要があります。
- イベントデータを別のクライアントや Synthesizer に再送信する場合は、Worklet 内で
Synthesizer.sendEventNow
を使用します。Worklet 内ではAudioWorkletGlobalScope.JSSynth.Synthesizer
とすることでSynthesizer
にアクセスすることができます。
- コールバックに渡されたイベントデータは
JSSynth.rewriteEventData
を使って書き換えることができます。
JSSynth
メソッド
waitForReady
Synthesizer エンジンの初期化を待機することができます。
戻り値: Promise オブジェクト (初期化完了時に Resolve します)
JSSynth.ISynthesizer
メソッド
(現在未ドキュメント化状態です。各メソッドの説明は dist/lib/ISynthesizer.d.ts
ファイルのJSDocコメントをご確認ください。)
ソースコード
https://github.com/jet2jet/js-synthesizer
ライセンス: BSD 3-Clause License (ただし externals
ディレクトリ以下を除く)