RSS Twitter Facebook


« 2018年11月 | 2019年01月のアーカイブ | 2019年02月 »

2019/01/30

SkinMan 1.01


8 年くらい前にリリースしてそのままになっていたお絵描きプログラム「SkinMan」にバグレポートが来て、そもそも何をやってたか思い出せるかどうかが危ぶまれたのだが、何とか直ったようなので、今更だけどバージョンアップしておいた。

SkinMan 1.01

バグの内容はファイルを保存する度に少しずつファイルが大きくなって行くというもので、今 300MB くらいになってるんだけど... という事で申し訳ないです。

長年放置してたので言ってもしょうがないけど、オーディオ系 GUI 作成に特化しているから今でも割と使いやすいとは思っているのだけど。ただ Win 専用だし、書き直すなら Web アプリにしたい所なんだけど、ファイルの互換性を維持とか、ちょっと大変そうなんだよな。

posted by g200kg : 2:18 AM : PermaLink

2019/01/28

MIDI 2.0 の件


さて、例年どおり開催された世界最大の楽器ショー NAMM 関係のニュースもほぼ出そろったようです。
島村楽器さんの記事とかRockOnさんの記事はまとまっていて読みやすいですね。見れば大体どんなものが出てきたかがわかりますが、気になるものはあったでしょうか?

私としては Teenage Engineering のモジュラー組み立てキットとか気になりますが。

そしてもうひとつ NAMM の直前くらいからニュースが流れていた「MIDI 2.0」の話。38 年ぶりに MIDI の規格がバージョンアップするという事です。といっても、「今やってます、名前は決めました」というニュースなので、これを搭載した製品が出てくるにはもう少しかかると思いますが、NAMM ではプロトタイプのデモなども行われたようです。


MIDI-CI

MIDI 2.0 に先立って昨年「MIDI-CI (MIDI Capability Inquiry)」という規格を作っているというニュースが流れていましたが、これとの関係としては MIDI-CI は MIDI 2.0 の一部として入口部分にあたるものになっています。下の図が MMA のサイトなどで公開されていますが、MIDI デバイスはまず MIDI-CI によってお互いにネゴシエーションを行い、MIDI 2.0 に対応している場合はそれを使って更に設定などの情報をやり取りする、対応していない場合は今まで通りの MIDI 1.0 デバイスとして接続する、という事を表しています。

MIDI 2.0 の基本的なコンセプトとして、3つの「B」と3つの「P」なるものがあります。

3つの「B」の1つが「双方向 (Bi-Directional)」です。今までの MIDI が一方通行で送りっぱなし、送ったメッセージが届いたかどうか、そもそも送り先にデバイスが存在しているかどうかがわからなかったのに対して、MIDI 2.0 では相手のデバイスとの間で双方向のやり取りをする事になっています。そして2つ目の「B」が「下位互換 (Backward Compatible」、つまり MIDI 1.0 の機器を繋いでもちゃんと動作する事。まあ、これを大事にしていたから MIDI はここまで生き残れてきたので当然の戦略ですね。

そして3つ目が「両方やる (Both)」。新しい MIDI 2.0 部分だけ作って MIDI 1.0 部分は手付かずというわけではなく、MIDI 1.0 相当部分にも可能な改善は加えていくという意味のようです。

そして双方向通信を使ってお互いが MIDI 2.0 に対応しているかどうかというネゴシエーション以降になる 3 つの「P」の部分が「プロファイル・コンフィグレーション (Profile Configuration)」「プロパティ・エクスチェンジ (Property Exchange)」そして「プロトコル・ネゴシエーション (Protocol Negotiation)」ですね。

プロファイルは自分が何の機器であるかを端的に表す属性を示すもので、自分は「オルガン」だよ、とか「アナログシンセ」だよ、という風に宣言する事で例えば「オルガン」ならドローバーの設定などを共有したりできるようにするというものです。

そしてプロパティエクスチェンジは、どんなパラメータを持っているかの詳細をやり取りするもので、今までの MIDI 機器でいえば、繋ぐ機器に応じたMIDI コントローラのセットアップとか、MIDI デバイスの MIDI ラーンの機能を自動化する事を目的としています。なおこれは、MIDI-PE と略される事はあるみたいですが、MPE だと MIDI Polyphonic Expression の略で、後述する ROLI の Seaboard なんかがサポートしている ノート毎のアーティキュレーションを指す機能の事になります。ちょっとややこしい。

3つ目のプロトコル・ネゴシエーションはこの双方向通信によって MIDI 1.0 デバイスと MIDI 2.0 デバイスを振り分ける仕組み自体をさしています。


ボイスメッセージ関係

ここまでは、MIDI-CI の周辺になる双方向関係の部分ですが、今回のニュースでは今までの MIDI の不満点として良く言われるベロシティのレゾリューションの不足への対応やチャンネルの増設などにも触れられています。

1 本のケーブルで 16 チャンネルに対応するというのが MIDI 1.0 の規格でしたが MIDI 2.0 では、1 本のデータのストリーム内に MIDI 1.0 または 2.0 として使用できる 16 個のグループがあり、各グループが 16 チャンネルを持っている、つまりトータル 256 チャンネルという事になります。

Group
12345678910111213141516
MIDI 1.0MIDI 2.0MIDI 2.0MIDI 1.0MIDI 1.0MIDI 2.0MIDI 2.0MIDI 2.0MIDI 1.0MIDI 2.0MIDI 2.0MIDI 2.0MIDI 2.0MIDI 1.0MIDI 1.0MIDI 1.0


ベロシティやコントロールチェンジのレゾリューションも拡張されて軒並み上がってますね。これらは MIDI 1.0 にも相当する機能が存在していますが、レゾリューションが拡張されています。記憶ではたしか MIDI 1.0 でも高精度ベロシティプリフィックス (14bit) とか一応あったと思うのですが、全然使われてなかったようです。

MIDI 1.0MIDI 2.0
ベロシティ 7bitベロシティ 16bit
ポリ・チャンネルプレッシャー/ピッチベンド 7bitポリ・チャンネルプレッシャー/ピッチベンド 32bit
16384 RPN (14bit)
16384 NRPN (14bit)
16384 レジスタードコントローラ (32ビット)
16384 アサイナブルコントローラ (32ビット)
128コントロールチェンジ (7bit)128 コントロールチェンジ (32bit)


そして MIDI 1.0 とは互換の無い形で新規に追加されるメッセージもあります。8bit の型と 16bit のデータを持つアーティキュレーション情報、256 種のレジスタードコントローラと 256 種のアサイナブルコントローラ。今までのコントロールチェンジがチャンネル毎にまとめて対応なのに対して、これらはノートの 1 音 1 音毎に対応するもので、「5 次元キーボード」という触れ込みで話題だった ROLI の Seaboard がサポートしている MPE に相当するもの、VST で言えば Note Expression のようなものになると思います。

MIDI 2.0 Messages
アーティキュレーション タイプ (8bit) データ(16bit)
256 拡張レゾリューション・レジスタードコントローラ (32bit)
256 拡張レゾリューション・アサイナブルコントローラ
拡張レゾリューション・パーノート・ピッチベンド (32bit)

これだけのものを規格としてまとめるのはなかなか大変そうです。流石に 38 年経って古いといわれる MIDI 規格ですが、MIDI 2.0 も長く使える規格になって欲しいですね。


posted by g200kg : 2:17 PM : PermaLink

2019/01/26

WebAudio.tokyo #7 が開催されました


昨日 1 月 25 日に渋谷で 7 回目となる WebAudio.tokyo が開催されました。

内容は LT が 5 本 + 飛び込みです。ピアノロールの実装とかシェーダーを使った VJ エフェクトとかシャトルラン用プログラムとか。ヒューマンビートボックスと 88 鍵 キーボードでプログラムを書く話が印象的でした。

会場はこちら。渋谷ヒカリエ 17F レバレジーズ様です。

@afroscriptさんのヒューマンビートボックスに連動する3Dの豚のデモ。

飛び込み LT の@fudafootaさん。88鍵キーボードでプログラムを書く話。画面真っ白になってるけど。

88 鍵に文字を割り当てると キーボード(楽器のほう)でプログラムが書ける!! そして「hello, world」が曲になる!! いいねー



LT枠がなかなか埋まらないみたいだったのでつい埋めに行ってしまったけど、なぜかこのイベントは飛び込みで面白いものを持ってくる人が多い印象です。
ちなみに私からのネタは「ConstantSourceNodeの話」です。内容は WebAudioAPI には「ConstantSourceNode」という目立たない地味なノードがあるのだけど、これが実は Envelope Generator ノードとして動作するという話です。

この辺のノウハウ的な話は他にもいくつかあるので暇を見て「Web Audio API 解説」に追加しておこうと思います。なお、Web Audio API 解説もサンプルコードが古くなったりしていたので現状にあわせて更新中です。

デモで使ったプログラムはこちら :

ただし、現在 Release チャンネルの Chrome 71 では、ある操作をするとブラウザがハングアップします。Canary の Chrome 73 ではなおっているので降りてくるのを待ち状態。

posted by g200kg : 4:34 PM : PermaLink

2019/01/16

サンプルコードは GitHub で Unlicense でどうかな?


ちゃんと纏まっているアプリではなくて、ちょっとしたコードフラグメントやサンプルコードのようなものを GitHub に置く時、ライセンスの選択ってどうやってますか?

だいたいいつもテンプレートで選べるメジャーな選択肢の中では一番緩いと思われる MIT License にしていたのですが、MIT にも表示義務はあるので本当のところは、使い方の Tips 的なコードフラグメントに MIT License が付いていても気にする人は困る事があるだろうなとは思っていたんです。

クリエイティブ・コモンズの CC0 とか、ちょっとネタ的ではあるけど WTFPL(Do What The Fuck You Want To Public License) あたりが適当かなと思うのだけど、GitHub のテンプレートには無いから自分でコピーしてくるのが面倒くさい。

と思っていたのだけど、良いのがあるのを知らなかった。

それが、Unlicense

法的制約に則りつつパブリックドメインに可能な限り近づけるという クリエイティブ・コモンズ CC0 と目指すところは同じようです。

CC0 の方が洗練されているとか、色々意見もあるようだけど、サンプルコードなんかにはこれで良くないかな?
GitHub のライセンステンプレートの一番最後に載っていたのに今頃気づいた。

という事で今後は Unlicense を推します。
https://github.com/g200kg/audioworklet-in-one-file

Unlicense 全文

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>


posted by g200kg : 9:22 PM : PermaLink

2019/01/15

[Web Audio API] AudioWorklet を 1 ファイルで書く方法



AudioWorklet は Web Audio API でユーザーが自分独自のカスタムノードを作成できるようにするための仕組みで、プログラムのメインスレッドで走らせる AudioWorkletNode と音声処理スレッドで実行する AudioWorkletProcessor と呼ばれる部分で構成されます。

基本的な書き方としては AudioWorkletNode 側と AudioWorkletProcessor 側を別ファイルの ".js" で書くというスタイルが標準的で、これは別に悪い事ではないのですが前身となった ScriptProcessor が単に関数を定義するだけで使えたのに比べると、ちょっと試しに軽く使いたい時に、別ファイルを準備するのがちょっと面倒くさいと感じてしまいます (大規模なプログラムを書くつもりなら分けておいた方が良いと思うんですが)。

という事で、1ファイルでメインプログラムとプロセッサをまとめて書く方法を幾つか試してみます。


今回ベースにするコード

まずはベースとして前回の記事 ([WebAudio API] AudioWorklet の使い方) で書いた AudioWorklet のオーバードライブを使用します。

プロセッサ側のコードは次のようになっています。

overdrive-proc.js

class OverDrive extends AudioWorkletProcessor {
    static get parameterDescriptors () {
        return [{
            name: 'drive',
            defaultValue: 0,
            minValue: 0,
            maxValue: 1,
            automationRate: "k-rate"
        }];
    }
    process (inputs, outputs, parameters) {
        let input = inputs[0];
        let output = outputs[0];
        let drv = Math.pow(0.05,Math.abs(parameters.drive[0]));
        for (let channel = 0; channel < output.length; ++channel) {
            for (let i = 0; i < output[channel].length; ++i) {
                var d=input[channel][i];
                if(d<0)
                    output[channel][i]=-Math.pow(-d,drv);
                else
                    output[channel][i]=Math.pow(d,drv);
            }
        }
        return true;
    }
}
registerProcessor("OverDrive", OverDrive);

プロセッサのコードは "OverDrive" クラスを定義して、registerProcessor() で名前を付けて登録する、という処理になっています。 このプロセッサを使うメインプログラムとして取り合えず簡単なものを書いたものが次のコードです。


<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>
<script>
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await audioctx.audioWorklet.addModule('overdrive-proc.js');
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

このコードのテストページ


音源を <audio> タグでドキュメント内に置いて MediaElementAudioSourceNode で読み込む方法で、オーバードライブを経由して鳴らします。ちなみに window.onload で初期化をしていますが、プライバシーポリシー制限でユーザー操作が無い状態で作った AudioContext は "suspend" 状態ですので、スライダーを触ったら resume() するという事をやっています。

プロセッサのファイル "overdrive-proc.js" を読み込んでいるのが 9 行目、audioctx.audioWorklet.addModule() です。


取り合えずプロセッサ側のコードを文字列として埋め込んでみる

まずはファイル "overdrive-proc.js" の内容をそのまま文字列に入れておいて DataURI 経由でアクセスするという方法です。38 行目の処理、
await audioctx.audioWorklet.addModule('data:text/javascript,'+encodeURI(overdriveproc));
がそれです。'data:text/javascript,' に続けてプロセッサの文字列をエスケープしてくっ付ける事で、URL としてアクセスできるようにしています。

単純に文字列としてコードを入れるのは、昔の JavaScript だと文字列が改行で途切れないように全ての行末に「\」を付ける必要があったりしてあまり実用的ではなかったのですが、ES6 でテンプレート文字列が使えるようになってからは、コード全体を 「`」で囲むだけですのでこれでもまあいけなくはないです。

このコードのテストページ

<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>
<script>
const overdriveproc =`
class OverDrive extends AudioWorkletProcessor {
    static get parameterDescriptors () {
        return [{
            name: 'drive',
            defaultValue: 0,
            minValue: 0,
            maxValue: 1,
            automationRate: "k-rate"
        }];
    }
    process (inputs, outputs, parameters) {
        let input = inputs[0];
        let output = outputs[0];
        let drv = Math.pow(0.05,Math.abs(parameters.drive[0]));
        for (let channel = 0; channel < output.length; ++channel) {
            for (let i = 0; i < output[channel].length; ++i) {
                var d=input[channel][i];
                if(d<0)
                    output[channel][i]=-Math.pow(-d,drv);
                else
                    output[channel][i]=Math.pow(d,drv);
            }
        }
        return true;
    }
}
registerProcessor("OverDrive", OverDrive);
`;
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await audioctx.audioWorklet.addModule('data:text/javascript,'+encodeURI(overdriveproc));
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

特殊なタイプのスクリプトとして HTML 内に埋め込む方法

他のやり方としては HTML 内に特殊なスクリプトとして埋め込む方法があります。スクリプトのタイプをそのまま実行される JavaScript と解釈されないように特殊な名前で定義します。

こういうやり方は WebGL の GLSL シェーダーを HTML 内に書く時などにも使われていて 'x-shader/x-vertex' とか 'x-shader/x-fragment' などとする事が多いようです。'x-' を付けるのは標準のタイプではない、という意味になります。 ここでは、AudioWorklet 用の JavaScript なので、
<script type="x-audioworklet/javascript">
としました。中身は document.getElementById('id').innerHTML で取ってこれますので処理としては文字列とそれほど違いはないですね。

ここの表示ではプロセッサのコードが一応 JavaScript としてシンタックスハイライトが効いているので文字列の場合よりかなり読みやすいですが、これはエディタによると思います。VScode では未知のスクリプト扱いで駄目でした。

このコードのテストページ

<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>

<script id="overdriveproc" type="x-audioworklet/javascript">
class OverDrive extends AudioWorkletProcessor {
    static get parameterDescriptors () {
        return [{
            name: 'drive',
            defaultValue: 0,
            minValue: 0,
            maxValue: 1,
            automationRate: "k-rate"
        }];
    }
    process (inputs, outputs, parameters) {
        let input = inputs[0];
        let output = outputs[0];
        let drv = Math.pow(0.05,Math.abs(parameters.drive[0]));
        for (let channel = 0; channel < output.length; ++channel) {
            for (let i = 0; i < output[channel].length; ++i) {
                var d=input[channel][i];
                if(d<0)
                    output[channel][i]=-Math.pow(-d,drv);
                else
                    output[channel][i]=Math.pow(d,drv);
            }
        }
        return true;
    }
}
registerProcessor("OverDrive", OverDrive);
</script>

<script>
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await audioctx.audioWorklet.addModule(
        'data:text/javascript,'
        + encodeURI(document.getElementById('overdriveproc').innerHTML)
    );
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

普通の JavaScript のクラスとして宣言してしまう方法

前の 'x-audioworklet' あたりの方法がまあ妥当かと思いつつ、最後にもうひとつトリッキーなやりかたで、普通に JavaScript のクラスとして書いてしまう方法もなくはないです。

もちろんそのままプロセッサのコードを メインのコード内に書いても動かないので少し細工します。

まずプロセッサ用のクラスは 'AudioWorkletProcessor' クラスを継承して作成しますが、この AudioWorkletProcessor はメインスレッド側には存在しないため、そもそもエラーになるのでダミーを宣言しておきます。
class AudioWorkletProcessor{}
それから次の 'addAudioWorklet()' がメインスレッド側のクラスをプロセッサとして登録するための関数です。やっている事は宣言されているクラスオブジェクトから 'toString()' で文字列化して、後は文字列として埋め込む方法と違いはありません。

ただし引数として取るのはクラスオブジェクトそのものですので、プロセッサ登録のための 'registerProcessor()' は自前で追加しています。また登録される名前はクラス名そのものになります。ここではクラス 'OverDrive' で宣言しているのでノードを作成する時も
new AudioWorkletNode(context, 'OverDrive')
となります。

addAudioWorklet()の戻り値は 'addModule()' と同じく Promise ですので必要なら await で待つなりしてください。

このコードのテストページ

<!doctype html>
<html lang="en">
<body>
<audio id="soundsrc" src="./loop.wav" controls loop></audio><br/>
Drive : <input id="drive" type="range" min="0" max="1" step="0.01" value="0"/><br/>

<script>
// Register Class as an AudioWorklet Processor
class AudioWorkletProcessor{}
function addAudioWorklet(context, proc){
    var f=`data:text/javascript,${encodeURI(proc.toString())}; registerProcessor("${proc.name}",${proc.name})`;
    return context.audioWorklet.addModule(f);
}

// AudioWorklet Processor Class
class OverDrive extends AudioWorkletProcessor {
    static get parameterDescriptors () {
        return [{
            name: 'drive',
            defaultValue: 0,
            minValue: 0,
            maxValue: 1,
            automationRate: "k-rate"
        }];
    }
    process (inputs, outputs, parameters) {
        let input = inputs[0];
        let output = outputs[0];
        let drv = Math.pow(0.05,Math.abs(parameters.drive[0]));
        for (let channel = 0; channel < output.length; ++channel) {
            for (let i = 0; i < output[channel].length; ++i) {
                var d=input[channel][i];
                if(d<0)
                    output[channel][i]=-Math.pow(-d,drv);
                else
                    output[channel][i]=Math.pow(d,drv);
            }
        }
        return true;
    }
}

// Main Program
window.onload = (async ()=>{
    audioctx = new AudioContext();
    await addAudioWorklet(audioctx, OverDrive);
    const src = new MediaElementAudioSourceNode(
        audioctx,{mediaElement:document.getElementById('soundsrc')}
    );
    const overdrive = new AudioWorkletNode(audioctx, 'OverDrive');
    const paramDrive = overdrive.parameters.get('drive');
    src.connect(overdrive).connect(audioctx.destination);

    document.getElementById("drive").addEventListener("input",(ev)=>{
        audioctx.resume();
        paramDrive.value=ev.target.value;
    })
});
</script>
</body>
</html>

これならエディタで確実にシンタックスハイライトが効くというのが良い所です。
メインプログラムのスコープ内にクラスオブジェクトとして存在してしまっているのが無駄と言えば無駄ですが、許せる範囲でしょうか。

posted by g200kg : 12:20 AM : PermaLink

2019/01/14

[WebAudio API] AudioWorklet の使い方



さて、2019 年になって ScriptProcessor の後継とされる AudioWorklet が Chrome で動き始めてからもう一年くらい経ちましたかね。まだまだガリガリ使われているという感じでもないのは、以前の ScriptProcessor に比べると構造が複雑になってちょっととっつきにくくなったからでしょうか。少しハードルが上がった感はありますね。

それに現状は Web Audio API の仕様とブラウザの実装がまだ少し乖離している所が残っているので気を付けないとハマるかも知れません。

とは言っても ScriptProcessor がいつまであるかもわからないし、そろそろちゃんと使わないとですね。

という事でまずはサンプルを書いてみました。

AudioWorklet (OverDrive)


たいしたものではないですが、これはオーバードライブエフェクトを AudioWorklet で実装したものです。[Play]を押すと音楽が流れて [Drive] のツマミでかかり具合を調整できます。


ソースコード overdrive-proc.js


class OverDrive extends AudioWorkletProcessor {
    static get parameterDescriptors () {
        return [{
            name: 'drive',
            defaultValue: 0,
            minValue: 0,
            maxValue: 1,
            automationRate: "k-rate"
        }];
    }
    process (inputs, outputs, parameters) {
        let input = inputs[0];
        let output = outputs[0];
        let drv = Math.pow(0.05,Math.abs(parameters.drive[0]));
        for (let channel = 0; channel < output.length; ++channel) {
            for (let i = 0; i < output[channel].length; ++i) {
                var d=input[channel][i];
                if(d<0)
                    output[channel][i]=-Math.pow(-d,drv);
                else
                    output[channel][i]=Math.pow(d,drv);
            }
        }
        return true;
    }
}
registerProcessor("OverDrive", OverDrive);


AudioWorklet では、音声処理を実行するプロセッサはメインのプログラムとは別ファイルで用意するのが基本です。

この 'overdrive-proc.js' がプロセッサ部分になりますが、AudioWorkletProcessor クラスから派生した class を定義し、registerProcessor() で登録します。プロセッサはメインプログラムとは別のオーディオ処理専用のスレッドで実行され、変数を受け渡したりというメインプログラムの世界とは直接的なやりとりはできません。

ここで定義したクラス内の process() 関数が実際の処理を行います。引数の inputs が音声信号の入力の配列、outputs が音声信号の出力の配列、parameters はこのノードが持っているパラメータの値を表します。OverDrive のようなエフェクター的なノードの場合はデフォルト状態の入力が 1 本、出力が 1 本で良いです(ステレオ音声等のマルチチャンネルも入力の数としては 1 本です)。

なので inputs[0] および outputs[0] が処理すべき入出力になります。入出力はブロック (128 サンプル) 毎に渡されますので、各チャンネルの各サンプルを入力から取得して処理を行い出力に渡しています。

ちなみにオーバードライブ処理というと一般的には tanh 関数なんかが良く使われるのですが、ここで行っているのは

\(output = Math.pow(input, 定数)\)

という処理を正負対称にくっつけたものを使っています。ここで定数を 1.0 ~ 0.05、つまり実数乗根とすると 1.0 で歪み無し、小さくなる程すぐに 1.0 に漸近するカーブになるので歪みが大きくなります。更にパラメータの 0.0 ~ 1.0 に対して冪乗の定数は

\(Math.pow(0.05, パラメータ)\)

で 1.0 ~ 0.05 の範囲にマップしています。入力に対する出力カーブは次のようになります。
まあこのあたりは好みもあるので適当ですが。

後は process() の最後が return true になっている所に注目です。とりあえず今は process() は true を返す、という事で良いですがこれはまた後述。

それから、static get parameterDescriptors() いうメソッドがありますが、これはこの OverDrive ノードが持っているパラメータを表しています。Oscillator ノードの frequency や Gainノードの gain のようなものです。

ここではdrive という 0 ~ 1 の範囲のパラメータを一つだけ持っていて animationRate が "k-rate" である事が宣言されています。

animationRate は "k-rate" と "a-rate" があり、"a-rate" はパラメータが 1 回の process() の呼び出し内でサンプル単位で変化し(する可能性があり)、"k-rate" はブロック単位なので process() 呼び出し内では必ず固定値になります。"a-rate" だともう少しコード量が増えてしまうし、"k-rate" より処理が重くなります。


ソースコード メインプログラム (html)

さて、プロセッサは登録しましたがこれを使う側です。
async function Init(){
    audioctx = new AudioContext();
    buffer = await LoadSample(audioctx, "./loop.wav");
    await audioctx.audioWorklet.addModule("overdrive-proc.js");
    overdrive = new AudioWorkletNode(audioctx,"OverDrive");
    overdrive.drive = overdrive.parameters.get("drive");
    vol = new GainNode(audioctx,{gain:0.5});
    analyser = new AnalyserNode(audioctx);
    src = new AudioBufferSourceNode(audioctx, {buffer:buffer, loop:true});

    src.connect(overdrive).connect(vol).connect(analyser).connect(audioctx.destination);

    //....
}
初期化の部分だけですが、async/await で書いています。
  • AudioContext の作成
  • 音源のロード
の次に
  • addModule
で、さっき作った "overdrive-proc.js" を追加していますが、この API は Promise を返しますので await で終了を待っています。これでメインプログラム側で OverDrive が使えるようになります。 そして
  • overdrive = new AudioWorkletNode(audioctx,"OverDrive");
が実際に AudioWorklet のノードを作成している部分です。これにより普通のノードとして使用できる AudioWorklet ノードが作成され、自動的にユーザーからは見えないオーディオ処理スレッド側では対応する overdrive-proc.js の OverDrive 処理のインスタンスが生成されます。

次の
  • overdrive.drive = overdrive.parameters.get("drive");
は、OverDrive ノードが持っているパラメータにアクセスしやすくするために overdrive ノードのプロパティにくっつけている処理です。これで、GainNode.gain と同様に OverDrive.drive という形式でアクセスできます。これについてはまた後述。

最後の行で、ノードの接続は

src(BufferSource音源) => overdrive(AudioWorklet) => vol(メインボリューム用Gain) => analyser(波形表示用) => destination

と普通のノードと同様に connect() で接続しています。後は音源用のデータを fetch で読んだり、ツマミをいじった時に オーバードライブのパラメータを変更したり、出てきた音の波形を Canvas で描いたりしていますけど、今までの WebAudio のプログラムと違う所は無いと思います。


気を付けないといけない事

今はまだ仕様と実装が完全に一致していない部分があったりするので幾つか注意点があります。
  • プロセッサの process() の戻り値は、ノードのライフタイムを制御します。

    仕様上は false を返した場合、ノードに信号が入力されている間だけ動作を続け、接続が無くなると自動的にノードが破棄される、という事になっています。これはその内、実装に反映されるのではないかと思いますが、今は false を返すとノードが止まってしまうので必ず true を返して動かしっぱなしにする必要があります。

  • 作成した AudioWorkletNode のパラメータにアクセスするには
    overdrive.parameters.get("drive")
    のように parameters.get() を経由する必要があります。例えば OscillatorNode.frequency や DelayNode.detayTime のようには行かない所が、まあ事情はあると思いますがちょっと残念ではありますね。
    このサンプルではせめて、という事でメインプログラム側でノードを作った後、
    overdrive.drive = overdrive.parameters.get("drive")
    とやって overdrive.drive でアクセスできるようにしています。これがプロセッサ側のコードでできれば良いんですけどね。

    アクセスした先のオブジェクトは普通の AudioParam ですので、.value に値を代入したり、setValueAtTime() などのオートメーション関数を使ったり、他のノードの出力を接続して変調を掛けたりという事は通常通りできます。

  • この例では使っていないのですが、AudioWorklet のノードのパラメータに作成と同時に初期値を渡したい時、少し書式が違います。

    例えば GainNode を作成する時、
    new GainNode(audioctx, {gain:0.5})
    とやって作成時に gain を 0.5 に設定するなんて事ができますが、AudioWorklet ノードの場合、例えば OverDrive の drive を 0.5 に設定するなら、
    new AudioWorkletNode(audioctx,"OverDrive",{drive:0.5});
    ではなくて、
    new AudioWorkletNode(audioctx,"OverDrive",{parameterData:{drive:0.5}});
    のようになります。これが一応仕様上定められた書き方ですが、仕様書内の EXAMPLE コードでも書き方が違っている部分があったりするので混乱しやすい所です。

  • AudioWorklet だけの問題ではないですが、去年導入された Privacy Policy の影響で WebAudio で音を出すには最初にユーザーの操作が無くてはいけません。AudioWorklet は動き始める前に AudioContext を使って必要なモジュールの登録処理等が必要ですので、処理の順序を考えるのが少し面倒になった感があります。まあちゃんと考えれば良いのですけど。

  • その内修正は入ると思いますが、基本的に Web Audio API の仕様書内にある EXAMPLE はまだちゃんと整備されていないのでそのままでは動かないものが多いです。

    今のところ実際に動作するコードのサンプルとして一番信頼できるのは、Google の WebAudio 開発者である @hoch さんが ChromeLabs で公開している Audio Worklet のサンプルページ です。

という事で、まだ少し気を付けつつ、ではありますけど AudioWorklet もそろそろガシガシ使えるフェーズに入りつつあるんじゃないですかね。

posted by g200kg : 3:40 AM : PermaLink

2019/01/09

Frizing でひっかかりやすい所


IoT とか Make 界隈で良く使われている Frizing という回路図エディタがあります。

基本的に初心者向けであまり大規模なものには向かないのですが、「ブレッドボード図」が簡単に描けるというのが特色で、ちょっとした工作の解説用の図を作る時なんかに重宝されています。

多分ブレッドボード図を描くために使っている人が大多数なのではないかと思いますが、機能としてはブレッドボード図、普通の回路図、基板のアートワーク、ガーバーの出力、おまけにソフトウェアのコードまでを一括管理しようというなかなか遠大な目標を持つツールです。

まあブレッドボード図が必要ないのなら Eagle なり KiCad なりを使った方が良いとは思いますが。

現在バージョンは 0.9.3b のベータ版で少しバギーな部分を残したまま既に2年半くらいアップデートが無いので今後の開発がどうなるかは少し気になる所ではあります。

frizing.org

それで、この Frizing で在り合わせの部品でブレッドボード図を描くだけなら、なかなか快適ではあるのですが、それ以上の事、独自の部品を登録したり、回路図を描いたりという事をやろうとすると色々問題点が噴出して一気にハードルが上がるんですよね。去年の年末あたりから割合ちゃんと触る機会があって、かなり問題を回避するコツを掴んできましたのでポイントだけ紹介します。


そもそも描きたい図はどれなのか


ブレッドボード図と回路図を描くのであれば、回路図=>ブレッドボード図の順序で書いた方が効率が良いです。

ブレッドボード図と回路図は一応並行で進める事もできるのですが、片方で描いた配線が反対側では仮接続のような状態になりますので、それが邪魔で回路図が書きにくい事があります。


作った部品のピンが勝手にショートしてしまう


新しい部品を登録する時、似た部品を基にして画像だけを差し替えて作る、という手法が良く紹介されているのですが、例えば GND や電源が複数本あって内部で繋がっているというような部品を基にしてしまうとその内部接続をそのまま引き継いでしまい、部品エディタの操作で修復する方法が無さそうです(見つけられないだけ?)。

例えば PIC16F883 の場合 PIN 8 と 19 がどちらも GND で内部で繋がっています。これの画像だけを差し替えて新しい部品を作っても PIN 8 に配線しようとすると 19 にもつながってしまいます。

PIN 8 につないだのに PIN 19 にも繋がっている。

PIN 数多めの部品は複数電源やGNDが複数出ている事が多いのでベースにするのは小さめのIC等で後から部品エディタで PIN 数を増やした方が良さそうです。


フォントの大きさがおかしくなる


これは良く知られた問題ですが、ブレッドボード図でも回路図でも、Inkscape で 0.1 インチグリッドに載せて SVG で描けば差し替え用の画像が描けますが、フォントの大きさが狂ってしまいます。

Inkscape が出力する SVG ファイル中のフォントサイズ指定が 'px' になっているのが Frizing で解釈できないのが原因のようなので、SVG をエディタで開いて font-size の 'px' 指定を消せばなおります。それも面倒ならすべてのオブジェクトをパスに変換してしまえば避けられます。どちらが良いかは一長一短。


回路図を描く時、ジャンクションが変なところについてしまう


特にブレッドボード図を先に書いて回路図エディタで仮接続を頼りに線を引いていると下の図のように部品の足の所にジャンクション(大き目の黒丸)が付いてしまいがちです。

これはトポロジー的には下の図のようになっていて部品間ワイヤーが2本以上集まっている所にジャンクションが付くという仕様のせいです。

下の図のように枝分かれさせたい場所にあらかじめ折れ曲がり点を作っておき、部品の足からその点に向かってワイヤーを引くという手順を取ると回避できます。なおこの時、ブレッドボード側の仮配線を一度消さないと、仮配線が邪魔で部品の足から出るワイヤーを選択できない事があります。

部品を登録する時にピン割り付けが面倒くさい

既存部品の画像を差し替えて新しい部品を作る時、各ピンがSVG画像のどこに対応するかを毎回指定するのが面倒くさいのですが、これは SVG 内のピンに相当するオブジェクトのプロパティで ID に "connector0pin"、"connector1pin" などの名前を付けておくと自動的に割り付けられます。部品エディタで各端子を表す ID 名 "connectorN" に "pin" を付けた名前です。わかりにくいですね。

なお、この機能はある程度認知されているようですが、うまく動作しないという報告もそれなりにあります。実際にやってみると、今のところおかしな挙動には遭遇していないのですが。

基板パターン図用の画像が差し替えできない

基板パターン用に使う SVG 画像は表面、裏面に配置するパッドをそれぞれまとめて "copper0"、"copper1" という名前を付けたレイヤーに入れておく必要があります。

更にこれは "プレーンSVG" 形式で保存した場合の話で、デフォルトの "Inkscape SVG" 形式で保存する場合は、各パッドをレイヤーではなく"グループ" でまとめてグループの ID に "copper0"、"copper1" という名前を付けると大丈夫なようです。

以上 Frizing で遭遇した問題の workaround 的ノウハウでした。

そもそもこのツール、ブレッドボード図を描くというニッチな所に特化している感じではありますが、コードエディタが統合されていたり、曲がりなりにもオートルーターが搭載されていたり、ガーバーが出力できたり、目指している所はなかなか凄いです。

今後に期待したいと思います。

posted by g200kg : 2:35 AM : PermaLink

2019/01/06

google ウェブサイト翻訳ツール


google が提供している翻訳系のサービスの1つなのですが、「ウェブサイト翻訳ツール」というのに久しぶりにアクセスしたら↓のような結果になって、どうやらサービスが終了の方向に向かっているようです。

google ウェブサイト翻訳ツール

この「ウェブサイト翻訳ツール」は良く使われているいわゆる「google 翻訳」ではなくて、サイト作成者がウェブページに仕込んでおくとページに英語やその他言語への切り替え機能を勝手に付けてくれるという無料サービスで、私も使っていたんですけどね。

下の画面の赤で囲ったのが言語切り替え用のウィジェットです。

で、これ、翻訳のエンジンは google 翻訳と同じか思いきや何か違うんです。何年か前、機械学習を使って google 翻訳の精度が飛躍的に向上した! というニュースが流れて、これでもう日本語で書いてウェブサイト翻訳ツールを入れておけば全て OK になるのか、と思ったらなんか違う。

左がgoogle翻訳で右がウェブサイト翻訳ツールです。全体的に google 翻訳の方がこなれていて、どうやら機械学習の恩恵はウェブサイト翻訳ツールには導入されなかったようです。

google翻訳でページ丸ごと翻訳するリンクとかを貼って似たような事はできなくもないのですが、それはそれで iframe の中に対応できないとか色々あって、結局待っていればウェブサイト翻訳ツールの翻訳精度が上がるのではないかと思っていたのですがね。残念ながらこういう事になったようです。

今後の方向として真っ当なやり方としては Google Cloud Translation を導入するというくらいなのかな。

まあインストール済みの翻訳ツールはそのまま動くという事なのでしばらく様子見ですけど。
---
最初のお断りのメッセージには「ネイティブで翻訳をサポートしているブラウザを使用されることをおすすめします」なんて書かれていますが、それは Chrome の事ですよね、というのはともかく、そういうブラウザ側で翻訳するのが普通になったりするんですかね? それはそれで楽な世界で良いなとは思うのですが。

posted by g200kg : 7:20 PM : PermaLink

« 2018年11月 | 2019年01月のアーカイブ | 2019年02月 »


g200kg