Programming Field

js-synthesizer

js-synthesizer はサウンドフォントを使ってMIDIシーケンスを波形に変換し、主にWebページ上で再生できるようにしたライブラリです。

English version is here.

[更新 2023/01/02: sample.js が一部のブラウザーで再生できない問題を修正しました。 / libfluidsynth を更新し、.sf3 ファイルを利用できるようにしました。]

サンプル

Soundfont:

SMF file:

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スレッド -- (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)に送信することができます。
  • AudioWorkletNodeSynthesizercreateSequencer メソッドで作成した 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 ディレクトリ以下を除く)