octahedron

LemとSKKとCommon Lispでたたかうプログラマのブログ

mimiumへのトランスパイラmmmmをつくっている

mimiumへのトランスパイラmmmmをつくっている

 今回はこのプロジェクトについてのお話です。

github.com

 つくった動機とか今後とかをメモっておく用途です。

mmmmとは

 勉強用につくった、独自言語(mmmm言語)から音楽プログラミング言語mimiumへのトランスパイラです。mimiumは松浦知也さん絶賛開発中の音楽プログラミング言語です。mimiumについては以下の拙作の記事を参照してください。

octahedron.hatenablog.jp

 で、mmmmとは何かというと、上記記事にも書いた問題点

ちなみに現在は「状態を閉じこめたオブジェクト」(たとえばクラスやレキシカルクロージャなど)に類するものはまだないので、上のシーケンサ的な状態を持つプログラムを複数つくるときには、実際に状態変数を必要な数だけ定義する必要があります。

をなにかしら(一時的に)解決するものとして、またあるいはコンパイラの勉強用として、作ってみるかーとやってみたプロジェクトです。「mmmm言語からmimiumへのトランスパイラ」と書きましたが、実際には現状ただのmimium→mimiumコンパイラというか、雑魚フォーマッタみたいなことになっています。

mmmmの動機

 なんとなくオブジェクト指向っぽく、defclass的なものとmake-instance的なものとobj.field的なアクセス記法を用意すればいけるかしら、という思惑で始めました。そして、まずは同じ意味論間でコードを吐けるようにするぞということで、現在mimiumのサンプルコードを同じ意味のコードに変換できる(つまり動作は変わらない)ようになりました。

mmmmの実装

 見ればわかるのですが、Rust製です。Rustはわりと書きやすくて、言語のさまざまな機能がCのような落とし穴は極力エラーにするような方針で設計されているためゆるふわ勢でもエグいバグに悩まされなくてよく、そして型システムや所有権などのチェック機構によりコンパイルエラーを取ればデバッグはふるまいの確認のみに集中できる、というところが気にいっています。低レベルのプログラミングにも使えそうなところも魅力です。

 mmmm言語は、現状はほぼ本家mimiumと同じですが、字句解析と構文解析にnomというRust製のパーサコンビネータを利用しています。

github.com

パーサコンビネータの勉強をしたいという動機もそういえばあったのでした。mmmmでは(同じ言語間の変換なので; つまりコードはASTをそのまま吐きだすだけなので)コードの比率的に現状パーサがほとんどを占めています。nomで基本的な枠組みをつくり、演算子の優先順位を決めるところだけ自前で操車場アルゴリズムを実装しています。nomというかパーサコンビネータは便利ですね。繰り返しや複雑な構文要素の組み合わせも基本的な関数の組み合わせで書けてしまい、書くぶんにはかなりすっきりしたコードになった気がします。ただコードがドキュメントみたいになってしまったので、意図通りの文法になっているかを保証する、あるいは意図した文法をBNFとかで明示する、とかしたほうがいいのかもしれません。

 コード生成については特に言うことはありません。なぜならそのままだからです。

 そんなに難しいことはやっていないので、初トランスパイラチャレンジなのを加味して2ヶ月くらいである程度うごかすところまで到達しました。

mmmmの今後

 さて、実はこの節がメインです。

 今後どうしようか、と悩んでいます。というのも言語の設計どうしたらいいかわからないからです。まず、オブジェクト指向的なものを入れてみるとして、構文的にあまり本家mimiumとバッティングしたくないなあという気持ちがあります。バッティングすると、いつかどちらかを削らないといけなくなり追従がむずかしくなりそうだなあというのがその理由です。第2に、そもそも「どうしたい」というモチベーション的な部分が弱かったので、自分がどうしたらいいかわからないのです。「オブジェクト指向的にすると実装は簡単そうだなあ」くらいの雑な動機で始めちゃった上に強く「こうしよう!!!」という芯がないので、mimium→mimiumコンパイルができた時点で道を見失いました。

 そんなわけでさっそく寝かせている状態です。さてどうしたものか…。


 書いていて気付いたんですが、よく考えたらmimiumでオリジナルのコードを書いたのはこのへん

が最後な気がするので、もうちょっとmimiumに浸って「こうなってほしい!」点を蓄積するのがよいのかもしれないですね。

mimiumさわってみた

mimiumさわってみた

 先週8/21にmimiumのv0.1.0がリリースされ、Ubuntu向けバイナリがでていたので触ってみました。

mimium.org

mimiumって?

 2019年のIPAの未踏プロジェクトに採択された音楽プログラミング言語です。未踏プロジェクトの成果報告会時のバージョンがたしかv0.0.1で、絶賛開発中の音楽プログラミング言語です。成果報告会での発表資料はこちらで、発表の動画はこちらから見れます。また作者の松浦さんによる開発記がこちらで読めます。

 発表によれば

楽家のための
音/音楽を簡潔に記述でき
高い実行性能を持ち
多様なプラットフォームで動作する
プログラミング言語を作りました。

とのことです。先行する音楽プログラミング言語とそれに対する利点等は未踏プロジェクトの成果報告会発表資料等をご覧ください。

 名前はMInimal Musical midIUMの略だそうです。

インストール

 mimiumの公式ドキュメントにあるように現在では導入するのに3通りの方法があります。すなわち

  1. homebrewでインストールする
  2. ビルド済みのバイナリをインストールする
  3. ソースからビルドする

 ぼくの環境はUbuntu 20.04でせっかくバイナリが配布されたところですので、2の方法でやっていきます。

 まずはmimiumのGitHubリポジトリのリリースページからバイナリを適当なところにダウンロードしてunzipしてください。そうすると展開結果はこんなかんじになります:

.
├── COPYRIGHT
├── LICENSE.md
├── README.md
├── bin
│   ├── mimium
│   └── mimium_llloader
├── lib
│   ├── libmimium_backend_rtaudio.so
│   ├── libmimium_builtinfn.so
│   ├── libmimium_compiler.a
│   ├── libmimium_runtime_jit.a
│   ├── libmimium_scheduler.so
│   └── libmimium_utils.a
├── mimium_logo_slant.svg
├── share
│   └── cmake
│       └── mimium
│           ├── mimium-config-noconfig.cmake
│           └── mimium-config.cmake

 このうちbinの中身をパスの通ったところに、libの中身を動的リンクライブラリのパスの通ったところに置きます。たとえばドキュメントにあるように/usr/local/配下にインストールしましょう。まずはsudo cp ./bin/* /usr/local/binとしてバイナリをコピーします。/usr/local/binはパスが通っているのでこれだけでOKです。つぎにsudo cp -r ./lib /usr/local/lib/mimiumとして.soを置きます。GNU/Linuxでは/usr/local/libはデフォルトで動的リンクライブラリのパスに含まれていません。また動的リンクライブラリのパスは再帰的に見てくれないので、以下のようにして/usr/local/lib/mimiumをパスに追加します。

$ echo '/usr/local/lib/mimium' | sudo tee -a /etc/ld.so.conf.d/mimium.conf
/usr/local/lib/mimium
$ sudo ldconfig

 こうするとmimiumを使えるようになります。

$ mimium
Error: Specify file name, repl mode is not implemented yet
return code: 0

触ってみる

 では実際に触ってみます。
 まずはリポジトリのexamplesフォルダに入っているデモを眺めたり実行してみたりして言語の雰囲気を楽しみましょう。ちなみにいくつかの.mmmファイルは実行するとno such file or directoryとエラーになりますがこれはmimiumが.wavファイルをロードできる言語でありプログラム中で指定した.wavファイルがないからです。適当に用意して名前を変えてやると動きます。

 ぱっと見て触ってわかるのは以下のようなことです。

  • サンプリングレートは48kHz
  • ALGOL系の文法
    • Rustみがある
  • 変数の型宣言は不要
  • 関数定義はfn name(args...) -> type {body...}の形式
    • 関数の返り値の型は(いまのところ)付けておいたほうがよい
    • よく見たら無名関数がある(adsr.mmm参照)
  • 関数呼びだしはname(args...)の形式
  • トップレベル(関数ブロックの外)で処理を実行可能
  • dspという名前の関数を定義すると、それが信号処理の毎サンプルを生成する関数になる
  • ifは式
  • nowという「現在のサンプル数」を返す特別な変数がある
    • (代入したらどうなるんだとワクワクして試したらError: syntax error, unexpected =, expecting end of fileと出るので、パーサで特別扱いしてそうな感じ)
  • selfという「selfが書かれている関数の直前の呼び出しで(直前のサンプルで?)返した値」を返す特別な変数がある
    • (いま発表資料を見ていて気が付いた。Faustにも(わかりにくいながら)ある機能らしい)
    • nowと同様、特別な予約語
  • 関数の実行タイミングをスケジューリングすることが可能
    • 関数呼び出しの後ろに@サンプル数とつけると指定したサンプル数のときに関数が実行される
    • kick_on()@10000(10000サンプルのときにkick_on()を呼ぶ)みたいな感じ
    • たぶんその文(式?)の実行時に引数を評価してスケジューラのキューにつっこむイメージ?
    • @の後ろには括弧で囲んで式も置くことができるので、サンプリングレートに応じた値とかを設定できたりする
  • sin()等いくつか組み込みの関数があるっぽい

と、このような感じです。仕組みとしては.mmmのプログラムをLLVM IRにコンパイルして、それを走らせて信号処理している感じであるっぽい。

 さて、音を取り敢えず鳴らしてみます。音を鳴らすためのしくみ等は説明しないので、拙作記事『音プログラムをつくる』あたりを参考にしてください。以下のmimiumコードをmimiumコマンドに食わせると、440Hzのサイン波が鳴ります。

$ cat test.mmm
sample_rate = 48000
pi = 3.141592
freq = 440

fn dsp(time) {
    sec = time / sample_rate
    ph = sec * pi * freq
    println(sec)
    return sin(ph)
}
$ mimium test.mmm
# ポーーーーー

 次に、リズミカルにノイズを鳴らすには以下のようにします。@によるスケジューリングのおかげでリズムマシンもわりと簡単につくれてしまいます。

// noise
noise_note_on = 0
fn noise_on() -> void {
    noise_note_on = 1
}
fn noise_off() -> void {
    noise_note_on = 0
}

fn trig_noise(dur) -> void {
    noise_on()@now
    noise_off()@(now+10)
    trig_noise(dur)@(now+dur)
}

fn dsp(time) -> float {
    n = 0.5 * (if (noise_note_on) random() else 0)
    k = 0.7 * (if (kick_note_on) kick() else 0)
    h = 0.7 * (if (hihat_note_on) hihat() else 0)
    return n + k + h
}

trig_noise(sample_rate / 2)@0
trig_noise(sample_rate / 8)@(sample_rate * 8)

 拙作のAltairKotoをつくったときは「シーケンサわからん」「シーケンサめんどくさい」と唸っていたので、これはお手軽に音楽ができそうです。

 ちなみに現在は「状態を閉じこめたオブジェクト」(たとえばクラスやレキシカルクロージャなど)に類するものはまだないので、上のシーケンサ的な状態を持つプログラムを複数つくるときには、実際に状態変数を必要な数だけ定義する必要があります。そのうち高階関数等が動くようになって楽になるかも的な話をされていました。

 以下のようにすると.wavファイルを再生できます。loadwav関数の戻り値は配列なのでwav[pos]のようにして値を取り出します。いまのところmimium全体がモノラル出力なので、.wavファイルも1chのものをご用意ください。

wavpath = "./ev.wav"
wav = loadwav(wavpath)
wavlen = loadwavsize(wavpath)
pos = 0

fn dsp() {
  v = wav[pos]
  pos = (pos + 1) % wavlen
  return v
}

まとめ

 mimiumのUbuntu版バイナリがでたので、ちょっと触ってみました。
 音楽のインフラを目指す言語ということで、mimiumのランタイムとPCMデータをパッケージして配布する(音楽は実行時の要素で変わる)というような、ただ再生するだけではない音楽の世界がmimiumによって形作られていく、かもしれません。
 今後の発展に期待です。

アセンブリ言語に入門したときのメモ

あらまし

 なんとなくアセンブリ言語に入門したのでそのとき詰まったこと等を記します。入門の成果である以下のコードを参照したりします。

my first (ARMv6) assembly program - gist.github.com

 まずはじめに、基礎知識を学ぶにあたって以下の本をまず通して読みました。

 アーキテクチャとして普段つかっているマシンのx86ではなくARMを選んだのは、x86こわいのでまずARMで慣れようという理由からです。あと手持ちのラズパイを活用したかったというのも。この本を譲ってもらっていて読みがかったというのも。
 この本では、いきなり単品のアセンブラファイルを書かずに、まずはgccインラインアセンブラ機能を利用してプログラムの一部をアセンブリ言語で書いていくのを薦めています。標準出力とかABIとかGNU/Linuxシステムコールとかの説明で煩雑になるし、覚えることがたくさんあるからのようです。

 しかし本に書いてあることはここには書きません。本でカバーされてない詰まったポイントとか追加情報とかを記していくつもりです。具体的には…

といったかんじのことを書きます。

Cコードなしのアセンブリコードの記述ルール

 本を一通り読んでインラインアセンブラを試したあとはCコード内ではなく直にアセンブリ言語を書いてみたくなりますが、それはどのような形をしていればよいのでしょうか。
 ぼくは最初、gcc -sの吐くアセンブラコードを参考にして(削って)つくろうとしましたが、かなりたくさんのアセンブラへの指示が書かれていてわからなかったので諦めました(どうもエントリポイントもgcc経由と直接asで異なるようでした。C言語の制約ぽい)。

 というわけでいろいろ調べたところ、ARMでは以下の形が最小のアセンブリコードのようでした。

        .text
        .global _start

_start:
        mov     r0, #0  // return 0;
        mov     r7, #1  // 1: sys_exit()
        svc     #0      // call sys_exit()

 .textELFの.textセクション、つまり機械語の命令列を置くセクションを意味します。これを書くことで以降の行の内容はオブジェクトファイルやメモリの.textに置かれます。セクションによって「プログラムとして実行可能」だったり「書き込み許可」など可能な操作が異なるようです。
 つぎに.global _startですが、これはアセンブル結果に含まれるシンボルを外部からも見れるようにする疑似命令です。モジュール外への関数のエクスポートのようなものと思っておけばよさそうです。ちなみに_startはラベル(後述)です。
 _start:は、アセンブルしたときのこの位置(アドレス)に名前をつけておくためのラベルです。前行の.global疑似命令で公開されているのでリンカでリンクする他のコードからも_startを参照することができます。GNU/Linuxにおいてはこれがプログラムのエントリポイントであるようです(がソース不明)。

 最後の三行はGNU/Linuxシステムコールexitを叩いてプログラムを終了しています。このやりかたはアーキテクチャABI毎に異なっており、man syscallに記載されています。ぼくのラズパイはarm/eabiなのでその方法に従っています。すなわち、

  1. どのシステムコール番号のシステムコールを呼ぶかはr7レジスタの数値で指定する
  2. システムコールへの引数はr0r5レジスタを用いる
  3. システムコールを呼ぶには上の準備をしたあとにsvc #0を実行する

ということです。システムコールがどの番号なのかはアーキテクチャによって異なるようで、これはヘッダファイルを調べるとよいようです。以下のようにgrepすると定義が見つかります(定義方法もアーキテクチャによって異なるので注意)。ちなみに命令のオペランドに、即値としての数値を記述する場合は、数値の前に#を付けるのがGNU asの記法です。

pi@raspberrypi:~ $ grep '__NR_exit' -R /usr/include/
# ココ! 「1」!!!
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_exit                 (__NR_SYSCALL_BASE+  1)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_exit_group                   (__NR_SYSCALL_BASE+248)
/usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit __NR_exit
/usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit_group __NR_exit_group
/usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit __NR_exit
/usr/include/arm-linux-gnueabihf/bits/syscall.h:#define SYS_exit_group __NR_exit_group
/usr/include/asm-generic/unistd.h:#define __NR_exit 93
/usr/include/asm-generic/unistd.h:__SYSCALL(__NR_exit, sys_exit)
/usr/include/asm-generic/unistd.h:#define __NR_exit_group 94
/usr/include/asm-generic/unistd.h:__SYSCALL(__NR_exit_group, sys_exit_group)

GNU asのディレクティブや記法のこと

 つぎは文字列の表示がしたいです。

 で、表示をするための処理のうちシステムコールwriteを呼べばよさそうなので、あとはメモリにデータを置くのみです。ldr命令とかでレジスタ-メモリ間はデータを移動できるのですが、メモリに直接データをおくにはどうするか。それはアセンブラやリンカに、起動時にデータを置くよう指示をします。これに用いるのが疑似命令(ディレクティブ)です。ここまでで.text.globalが登場しています。ディレクティブの一覧は以下のマニュアルを参照します。

Using as - Assembler Directives

 ここでは.dataセクション(読み書き可能なデータを置くセクション)を用意して、そこに文字列を置き、それをsys_writeで表示してみます。プログラムはこんな感じです。

.data
hello:
        .byte   0x68  // h
        .byte   0x6f  // o
        .byte   0x67  // g
        .byte   0x65  // e
        .byte   0x0a  // newline
hello_len = . - hello

.text
.global _start

_start:
        ldr     r0, =hello
        mov     r1, #42
        strb    r1, [r0, #1]

        mov     r0, #1          // file discripter
        ldr     r1, =hello
        mov     r2, #hello_len  // character count
        mov     r7, #4          // sys_write
        svc     #0

        mov     r0, #0
        mov     r7, #1
        svc     #0              // sys_exit

 そういえば、コメントは上の本(のインラインアセンブラ章)には#が使えるとあったのですが、アセンブリコードにはつかえなさそうでした。//がコメントとして使えるようなので利用しています。
 .data領域にhello:というラベルをつくり、それ以降に文字を1バイトずつ置いています。hello_len = . - helloはシンボルhello_lenに、記述位置.からhello:の位置を減算した数値(つまり文字列のバイト数)を設定(代入?)しています。より親しみやすい記法で文字列を書ける.stringというディレクティブもありますが、今回はメモリに置いてるっぽさを重視して使いませんでした。
 _start:すぐ後の三行で、hello:のアドレスをldr命令でレジスタにロードしています。ラベル名に=がついていますが、これは=以降にラベル等の評価後数値になるシンボルがきたときそれを即値として記述できる、というGNU asの記法のようです("ARM Opcodes (Using as)"参照)。

 ちなみについでにstrb命令でメモリの書き換えも実験しています。第2オペランド[r0, #1]はメモリアドレスの記法です。「r0レジスタのアドレスに1足したアドレス」という意味です。このへんはARMの命令セットチートシートがさくっと見れてよかったです。

アセンブリコードの実行方法

 上記のhogeを出力するコード (hoge.s) を実行してみます。

まずはラズパイ上で

 GNU asの使いかたがよくわからなかったのですが、以下のようにすれば(ちょっと長いけど)アセンブルとリンクができます。

# アセンブル
$ as hoge.s -o hoge.o
# リンク
$ ld hoge.o -o hoge
# 実行
$ ./hoge
h*oge

 GNU asにas hoge.sとするとa.outができたりするのですが、リンカがエラーを吐くので上のようにしました。

Ubuntu (x86)上で

 ラズパイだとスペックが低いためGNU Emacsやlemが重いので、x86上のUbuntuでもエミュレーションとかで動かせないかを試してみました。

 QEMUユーザーモードエミュレーションを使います。QEMUにはマシン全体をシミュレーションするモードとGNU/Linuxのユーザー環境をエミュレーションするモードがあるようで、こちらを利用すれば、HDDイメージにUbuntuをインストールしておく手間を省いて開発できます。

 利用するには、まずQEMUとARM向けのgccをインストールします。

$ sudo apt install qemu qemu-user-static gcc-arm-linux-gnueabi

 インストールが終わったら、ARM用のas/ldを用いてリンクまで行い、qemu上で実行します。

# アセンブル・リンク
$ arm-linux-gnueabi-as hoge.s -o hoge.o && arm-linux-gnueabi-ld hoge.o -o hoge
# 実行
$ qemu-arm-static -L /usr/arm-linux-gnueabi ./hoge
h*ge

まとめ

 アセンブラたのしい!!

 マシン語は広大だわ…
 なにをつくろうかしら…

退職します

 現職を今月末付けで退職します。

 現職に入社したのは2018年の1月ごろだったと思う。
 入社当初はモダンフロントエンド開発やJavaScriptに初めて触れて、こんな世界もあるのかーと感動した記憶がある。reduxわからない日々が続いたのも覚えてる。Unity管理画面というチャレンジもやったなあ。わりと得意分野の関係もあってか業務的にはサーバーサイドにシフトしていったのでだいたいはサーバーサイドしていた気がする。お花見をお断りして開催したRust勉強会(発表内容ボロボロ)もやった。
 前職に引き続きドキュメンテーションはやいのやいの言って改善に向けた気がする。新プロダクトを作ろうとしてた時期もあったっけ。あのとき、もっといっぱいコンセプトについて話していればなあ。

 今はもう退職された/今もいるメンバーとはいろいろな議論をしたなあ。意見を本当に自由に言える職場で、「ぼくが自由すぎたのでは??」とあとから反省することもあるくらい、みんな寛容だった。意見がすれ違う理由が、立場や考え方の違いであったり、他方の好みだったり、あるいは好みのぶつかりあいであったりもした。

 あとはプログラミング歴が浅いメンバーの入社は、なかなか手探りだった。知人はわかると思うがぼくは趣味が初心者志向ではないというか偏ってるので、相手を不安にさせたり混乱させたり、あるいは苛立たしさとかあったかもしれない。あとGNU Emacsの布教が全然さりげなくなくてしつこかったことは正直すまんかった。でもそれぞれこの妙に変な方に傾いたぼくについてきてくれたのはありがたかった。

 ちょっとリーダー的なことはしたけれど、チームビルディングはまだなんにもわかっていない。わかってないけど反省を伴った実感として「自分たちはどうありたいだろうか」ということを業務内容に直接絡まないとしても話す場を作ったほうが、種々細々の決断(方針、仕様、技術その他判断)が説得力あったかなあとは思った。またリーダー的なことをやることになったら、試してみよう。その上でなにか良い本教えてください

 辞める理由は、実は結構多層的で、Twitter見てる人は知ってるだろうけど転職活動はゆったりながら一年半前くらいからしてた。経営層のやりたいことや方針がぼくには理解できなかったり、たまに理不尽に見える判断がなされたり、まあ人並みに不安を溜め込みそうなことが続いた結果だ。あとは、常々なぜだか意見を違えまくるのにも疲れてきた。同僚に「趣味嗜好が特殊なの意識したほうがよいのでは」と言われたりするくらいにはズレているので、時折判断が普通じゃないように見えたりしてただろう。それらが積み重なった結果ふと「あ、ぼくもういなくても問題ないな」と、拗ね、病み、そして立ち直ってなおその思いが変わらなかったので、ついに、転職活動に本腰入れた。
 ちなみに「拗ね、病み」の時期は職場的にも扱いかねる感じでご迷惑かけたと思う。心はいったん折れていたので全てを遠ざけたかったのでした。

 次の職場もなんとか決まり(若干レイヤーが下がる)、退職日も決まり、自分で会社を一時的に拒絶しちゃったためちょっと閑職でつらいけど、いい思い出でした。

 先に旅立っていったメンバーたちのところに、ぼくも行くぞ。

https://www.amazon.jp/hz/wishlist/ls/4RJ9BZGEYLGT?ref_=wl_share

———

翌深夜追記: 退職ミニアルバムも作ったよ。合わせて是非! http://sinclav.bandcamp.com/album/goodbye-ep

スパイシーカレーを自作する

 ここ一番のカレーができたため、そのレシピを忘れないようにメモした記事です。

背景

 神保町の三省堂のある交差点にあるカレー屋さんビストロべっぴん舎のスパイシーなカレーがとてもおいしくて、ふとカレー自作してみると家でも似た味をつくれるのではと思い、自作カレーを始めました。本を買い載っているカレーをいろいろつくってみたり、TOKIO俺たちのDASHカレーをルーづくりから挑戦してみたりといろいろやっているのですが、現時点の最高傑作ができあがったのでここに記しておく次第です。

注意

 わりと下拵え手順が多いうえ全体の調理にも時間がかかるので、2時間くらいかかると思っておいたほうがいいです。あと、材料はぜんぶ皿に計って出しておくと慌ててもたもたしないのでおススメです。

 以降のレシピでは2つの情報を参照します。1つめはぼくのスパイスカレーの参考書。

pie.co.jp

 以降に記す作り方も細部はこの本を参照します。

 あとはTOKIOのDASHカレーの要素も取り入れています。

www.ntv.co.jp

材料(2〜3皿分)

 調理手順の順に書きました。適宜野菜とかキノコとかいれるとより楽しめます。

  • サラダ油 … 大さじ3
  • ベースの風味を出すスパイス
    • シナモン(枝) … 1/2本
    • クミンシード(ホール) … 小さじ1
    • ブラックペッパー(ホール) … 小さじ1
    • クローブ(ホール) … 7粒
    • カルダモン(ホール) … 5粒
    • マスタードシード(ホール) … 小さじ1
  • ニンニク … 4個
  • ショウガ … 1片 (15gくらい)
  • タマネギ … 1個
  • トマト … 大 or 中 1個
  • メインの風味を出すスパイス
    • ターメリック(パウダー) … 小さじ1/2
    • レッドチリ(カイエンヌペッパー) … 小さじ1
      • これは辛さを決めるので調整可。本では小さじ1/2だが、ちょっと辛めにするため本レシピでは追加している。
    • パプリカパウダー … 小さじ1
    • クミンシード(パウダー) … 小さじ2
    • コリアンダーシード(パウダー) … 小さじ2
    • フェヌグリークシード(パウダー) … 小さじ1
    • ナツメグ(パウダー) … 小さじ1
  • 塩 … 小さじ1.5
  • 水 … 400ml
  • コンソメブロック … 2個
  • 鶏もも肉 … 1枚分
  • リンゴ … 1/4個
  • 香りを出すスパイス
    • サラダ油 … 大さじ1.5
    • マスタードシード(ホール) … 小さじ1
    • パプリカ(パウダー) … 小さじ1

手順

概要

 上に書いた水野本のカレーメソッドの以下のレシピをアレンジしたものになります。

  1. 1-C(しみじみ深みのある香り)
  2. 2-B(スタンダードなベース)
  3. 3-B(トマトの王道なうま味)
  4. 4-B(スタンダードでメジャーな香り)
  5. 5-B(ブイヨンでノスタルジック)
  6. 6-A(ブライニングして煮るチキン) or 6-B(こんがり焼いて煮ないチキン)
  7. 7-C(力強く刺激的な香り)

0. 下拵えする

 スパイスや調味料を計って皿に出しておく。
 ニンニクとショウガはすりおろしておく。リンゴもすりおろしておく(ニンニク・ショウガと入れるタイミング異なるので注意)。  タマネギは薄切りにしておく。トマトは切って、できれば潰しておくとよい。

 鶏肉はぶつ切りにし、6-A(ブライニングして煮るチキン)なら水400mlと塩小さじ2と砂糖小さじ2を入れたボウルで2時間くらい放置(この工程がブライニングらしい)。
 6-B(こんがり焼いて煮ないチキン)なら、鶏肉に塩コショウを振ってちょっと置き(その間に上の行の下拵えしとくといいです)、フライパンに油(分量外)を入れて焼き色をつける。あとは投入まで放置。

1. ベースのスパイスで香り出し

 サラダ油とベースのスパイスを鍋に入れ、弱火と中火の中間くらいで香りをだす。スパイス・香辛料の香りは水溶性と油溶性があり、油溶性のほうの香りを油に移すのが目的。
 クミンやマスタードが濃い茶色になり、カルダモンがぷっくりし、マスタードがパチパチ弾けはじめたら本工程完了。

2. ベースのうま味・甘味をだす

 すりおろしたニンニクとショウガを入れ、中火に。キツネ色になったらタマネギを投入し強火にする。キツネ色になるまで炒める(20分くらいかかる)。
 タマネギの炒め方は水野本に詳細が書かれているのでそちら参照。ざっくりいえば、水分を飛ばし甘みを増加させるためにねっとりとなるまで炒める感じ。

3. トマトのうま味をだす

 切ったトマトを木べらで潰しながら中火で炒める。水分を飛ばしてうま味を凝縮させる(10分くらいかかるかも)。
 木べらで混ぜてもトマトが崩れたりてろってならないペースト状になるまで炒める。

4. メインのスパイスを入れる

 メインのスパイスと塩を入れ、ペーストに炒めて混ぜ込む。2分くらい。

5. 水とダシを投入する

 水とコンソメブロックを入れ沸騰させたら、弱火で5分煮る。これでペーストの味がいいかんじに混ざる。

6. 鶏肉を入れる。

 6-Aか6-Bで分岐。このときにリンゴも入れる。
 ちなみにぼくはおなかすいてるときは両方やります(6-A→6-B)。野菜類は、根菜は6-Aのときに、ナスなど煮すぎると色が変わってしまうものは6-Bのときに入れてます。

6-A. ブライニングした鶏肉を投入

 ブライニング(浸透圧で鶏肉に水分と下味をしみこませ、ふっくらさせる技法)した鶏肉を、水を切って鍋に入れる。
 沸騰したら弱火で20分間煮る。

6-B. 焼いた鶏肉を投入。

 鶏肉を、焼いたときに出た油ごと鍋に投入。弱火で3分から5分煮る。

7. 香りスパイスを投入

 香りづけのため、マスタードシードとパプリカパウダーをテンパリング(油に香りを移す)をする。
 サラダ油大さじ1.5とマスタードシードをフライパンに入れ、パチパチしてきたらパプリカを入れる。パプリカの香りが立ってきたら油もそのままカレーの鍋に投入する。パプリカを焦がさないように注意。

完成

 ちなみにお米はタイ米だとサラっとしたルーに合うのでおいしいです。

まとめ

 めっちゃうまい!!!!

Lispエイリアン壁紙をつくりました3

3年弱ぶりに新作つくりました。

ジェネラティブアートの手法を使ってつくるつもりが脱線した結果こうなりました。ついでに元の画像編集ソフトのファイルも合わせて過去の分も合わせてGitHubに置くことにしました。ライセンスどうしたらいいかわからないけどとりあえずCC0にしてます。

GitHub - t-sin/lisp-alien-wallpapers: DON'T PANIC! These are made with secret alien technology.

ご査収ください。

音プログラムをつくる

 先日(2019年10月ごろ)音プログラムをつくりました。

github.com

 こいつ自体はCC0でリリースしてるのでソースコードから知識をどうぞパk参考にしてやってください。すごい音プログラムではないけど、こういうプログラムを作るときに考えることや知っておくといいことなどを記事として残しておきます。

 どうでもいいけどロゴかわいいのつくれました。7月に生まれた東武動物公園のマレーバクの男の子アルタイルくんです。

f:id:t-sin:20191013234248p:plain
音プログラムAltairのロゴ

ステップ1: どんな音プログラムにするか考える

 まずは仕様を固めましょう。
 生成した音はリアルタイムにスピーカーで鳴らすのか、WAVファイル等に書き出す方式なのか。音の生成方法はどのように指定するのか。インタラクティブに音の生成方法を変更できるようにするのか。ユーザインターフェースはどうするのか。

  まずはモチベーションを整理します。今回Altairをつくるに至った理由として、以下のような思いがありました:

  • Nim version 1.0が出たのでなんかしたい
  • 音プログラムにしたいけど音作りに凝れなくていい
  • 自作言語で挙動(音色・メロディ等)を指定したい
    • Forth/PostScriptはいいぞ
  • しかしあまり時間をかけたくはない
    • パラメータの時間変化をいれたいが、めんどくさそう
    • 譜面データを自作言語で合成(繰り返しとか)したいが、めんどくさそう
    • 自作言語を多機能にしたいが、めんどくさそう
  • 音はリアルタイムで鳴ってほしい
    • しかしインタラクティブにするとなにかしらのインターフェースを考える必要があってめんどくさそう

 そこでAltairの場合は以下のような仕様にしました。

  • 音はスピーカーから鳴らす
  • 音の生成方法はPostScriptっぽい言語で指定する
    • この言語は関数定義できないくらいの弱い言語にする
  • 音の生成方法をインタラクティブに変更しない (=UIなし、マルチスレッディングなし)
  • ただし、REPLで音の生成方法をまるごと挿げ替えることはできる

 加えて、音の生成方法について以下のことを考えました。

  • 音を出すオシレータは正弦波とノコギリ波とノイズくらい
  • メロディ/リズムを指定するシーケンサはかなり簡単なもの
  • モジュラーシンセみたいなモジュレーションは一切搭載しない
    • パラメータは初回のみ指定でき、時間方向の変化はしない
  • 再生時にシーケンサのループ再生はしない
    • これは拍子情報を持つか持たないかの判断基準になる
    • 今回は拍子情報を持たない

 わりとシンプルなものになりそうです。

ステップ2: ピーって鳴らす

 まずは音のhello worldということで、とりあえずピーと鳴らしてみます。
 ちなみにこのAltairはNimという言語で書きました。先日version 1.0が出たのでうれしくって書きはじめたのがAltairなのでした。なので引用するコードは(過去のコミットログから引っぱりだしてくるので)Nimで記述されています。まあ雰囲気で読めるでしょう。

 これがノコギリ波でピーしたときのプログラムです。

github.com

 オーディオ入出力にはクロスプラットフォームなオーディオライブラリのPortAudioを用いることにしました。やっていることはだいたい以下のような感じです。

  • 波形を鳴らすときの状態を保持するオブジェクトを定義する
  • 上記オブジェクトを用いて信号を生成する関数を定義する
  • オーディオライブラリを初期化する
  • 信号を生成する関数をオーディオライブラリに設定する

 これらを詳しく説明しつつ、オーディオライブラリの仕組みや使い方についても説明します。

オーディオライブラリの仕組み

 コンピュータにおいて音をスピーカーから出すにはプログラムのパラメータやユーザの入力から最終的には電圧の変化を作りだす必要があります。しかしそのへんは各サウンドバイスを含めてOSが、さらにはオーディオライブラリが抽象化してくれています。われわれがする必要があるのは

  • 信号についての情報(サンプリング周波数など)を与えて
  • 信号そのものを逐次ライブラリに渡してやる

ことです。

 そのために、まず音がコンピュータ上でどのように扱われるのかを知る必要があります。音は連続なデータですが、コンピュータ上では離散的なデータで擬似的に表現します。連続な音を一定時間ごとにサンプリングし、そのサンプリングした数値の列として表現します*1。このとき音をサンプリングする時間間隔のことをサンプリング周波数といいます。CDだとサンプリング周波数は44.1kHzと決められています。サンプリング周波数はオーディオライブラリ初期化時に指定し、音が鳴っている間変化しない値です。
 サンプリング周波数は時間方向に信号を離散化する際の情報です。コンピュータで離散的にオーディオデータを用言するには、サンプリングされた値を大きさの方向にも離散化(量子化といいます)することも必要です。値を何bitで量子化するかということで量子化ビット数といいます。オーディオライブラリにおいては、サンプル値を表現するのにどの型 (char, int, float, ...)を使うのかで指定します。Altairではsingle floatでサンプル値を表現しているので32bitです。

 信号そのものをライブラリに渡す方法ですが、1サンプルずつ渡すのではなくある程度の量のサンプルをまとめて渡す方式がとられることが多いです。理由はサンプル計算から実際の音出力までのオーバーヘッドを少なくするためです。プログラムがサンプルを計算してからデバイスに渡るまでにオーディオライブラリ、OSによる仲介、デバイスドライバ……といろいろな抽象化レイヤー経て信号データがスピーカーに送られていくのですが、これを1サンプル毎にやっているとオーバーヘッドが大きくなり処理が追いつかなくなってしまうという寸法です。
 上で示したhello worldコードではPA.OpenDefaultStream手続きの引数でframesPerBufferとして渡しているのがそのバッファのサイズです。

音生成のモデル

 そこそこ複雑な音プログラムでは音生成の各部分がモジュールとして独立しており、これを組み合わせて複雑な音作りをする、というアプローチを取られることが多いです。音を波形を生成したり(オシレータ)、入力された波形を処理して出力したり(エフェクト)といった基本的なモジュールを多数用意し、それらを繋ぎ合わせるのです。この方式はユニットジェネレータと呼ばれています *2

f:id:t-sin:20191014134931p:plain
ユニットジェネレータのイメージ: Bitwig Studio 3のモジュラーシンセシステム

 音を鳴らすプログラムでは状態の管理が重要です。
 波を生成するオシレータではいま波のどの時点まで計算したか、という状態を持つ必要があります。ディレイやフィルタなど、入力を遅延させてごにょごにょする系のエフェクトでも、一定長の信号をバッファに入れておく必要があります。
 このような状態管理は、オブジェクト指向のアプローチで実装するとうまくいきます。ユニットジェネレータにもオブジェクト指向的な側面があり、このアプローチで実装するとうまくいきそうです。

 ユニットジェネレータ方式では複雑な音作りを実現するために、各ユニットジェネレータのパラメータ部分の値として別のユニットジェネレータの出力を使えるようにしてあるものがほとんどです。このようなやりかたをモジュレーションといいます。

 今回Altairではユニットジェネレータ方式を採用しつつ、モジュレーションのような柔軟な音作りはしない方針で設計を行いました。

ノコギリ波の生成

 ノコギリ波を生成します。ここだけ上のコードとは式が異なることに注意してください(floorの方法を当時知りませんでした)。ノコギリ波とは以下の図のような形の波形です。

f:id:t-sin:20191110232558p:plain
ノコギリ波の波形

 時間が0のとき値が最低点で、1周期すぎていく間に最高点まで値が上がっていく波形です。ここでは周期を1としておきましょう。そうすると、0から1までの間で、以下のような計算をすればよいです。

{ \displaystyle
{\rm saw}(t) = t - {\rm floor}(t)
}

 ここでfloorは小数部を切り捨てる関数です。この関数の値域は0から1ですが、音の信号は(浮動小数点型においては)-1.0から1.0の間で表わされることが多いので値を調整すると、こうなります。

{ \displaystyle
{\rm saw}(t) = 2 ( t - {\rm floor}(t) ) - 1
}

ユニットジェネレータの定義

 さて、実際にユニットジェネレータを定義していきましょう。エフェクトを繋ぎたくなるかもしれないのでユニットジェネレータはとりあえず「前」があるように作っておきます(と書いたけれどけっきょく使ってませんでした)。値の計算のときには前の値をそのまま出力するようにしておきます。

type
  Unit = ref object of RootObj
    prev: Unit

method procUnit(u: Unit): Signal {.base.} =
  return u.prev.procUnit()

 次にこれを継承してノコギリ波オシレータ用のユニットジェネレータをつくります。オシレータなので「前の結果を流す」というUnitの処理は呼びません。Sawオブジェクトのphaseスロットがオシレータの進行状態 (位相角) を保持しています。

type
  Saw = ref object of Unit
    phase: float32

method procUnit(u: Saw): Signal =
  u.phase += 0.01

  var
    ph = u.phase mod 1.0f32
    s: Signal

  # 古いコードなのでfloorつかってない
  if ph <= 0.0f32:
    s = (1.0f32, 1.0f32)
  else:
    var v = -2 * ph + 1
    s = (v, v)

  return s

 あとはユニットジェネレータオブジェクトの初期化やオーディオライブラリの初期化、信号バッファを埋めるコールバックを書けば音が出るようになります。

音程を指定する

 ここまでのコードは波形を出すことのみに集中していたので、音程のことは考えていませんでした。実際に音楽をつくるには、まず周波数によってオシレータの音程を指定できる必要があります。ここではオシレータユニットを定義しパラメータとして周波数を設定できるようにします。

 音程は440Hzがラの音です*3。はじめは周波数を直接指定するとして、オシレータの内部状態の位相角を周波数に従い正しい値で更新できるようにします。

 各サンプルの値を繰り返し計算することで信号を生成するため、信号の時間単位はサンプリング周波数です。このプログラムではサンプリング周波数を44.1kHzとしているため、1サンプル進むと1/44100秒が経過します。1Hzの信号を出力したい場合は44100サンプルで1周期進めばいいため、位相角の差分は1/44100です。2Hzのときは2 / 44100 = 22050サンプルで1周期すすめばよいため位相角は1/22050となります。nHzのときはn / 44100サンプルで1周期すすめばよいわけです。
 したがって以下のように作っておくと任意の周波数でノコギリ波を鳴らすことができます。

# だいたい https://github.com/t-sin/altair/blob/71c6ed46663cbd36b665b2cda55d1d719ba7d3de/src/altair/ug.nim#L39

type
  Osc* = ref object of UG
    phase*: float32
    freq*: float32
  Saw* = ref object of Osc

method procUG*(ug: Saw, mi: MasterInfo): Signal =
  var
    ph_diff = 440 / mi.sampleRate
    ph = ug.phase
    s: Signal

  var v = 2 * (ph - floor(ph)) - 1
  ug.phase = ph + ph_diff
  (v, v)

ステップ3: 音づくりの幅を広げる

 まずは音が出るようになりました。しかしとても理想的なノコギリ波なので音が細くて物足りないです。せめて倍音を豊かにしたりしたい。
 そこで、信号同士の演算を実装して音をつくれるようにしてみようと思います。

 現状は単音しか鳴らせません。そこで複数の音を同時に鳴らせるようにしましょう。複数の音を同時に鳴らすということは、オシレータ等の音を出すものが複数あってその出力が足し合わさればいいわけです。これをMixユニットとして実装しているのがこのコミットです。

github.com

 このコミットの中ではデチューン技が使われています。2つのオシレータの周波数 (=音程)をわずかにずらすことでうなりを発生させ、1つのオシレータで鳴らすより広がりのある音をつくることができます。

var
  saw1 = Saw(phase: 0, freq: 440)
  saw2 = Saw(phase: 0, freq: 445)
  mix = Mix(sources: @[saw1.UG, saw2.UG], amp: 0.2)

 Mixユニットでは信号を足し算しました。たとえば2つの信号を掛け算するとどのような音になるでしょうか。これは振幅変調 (AM; amplitude modulation) あるいはリング変調 (ring modulation)と呼ばれる方法です*4
 以下のコミットでは(後で説明するエンベロープジェネレータも入っていますが)信号を掛け合わせるMulユニットを実装しています。

github.com

 ユニットは以下のように結線されています。

var
  saw1 = Saw(phase: 0, freq: 440)
  # こいつは後で登場します(エンベロープジェネレータです)
  env = Env(adsr: Release, eplaced: 0, a: 0.1, d: 0, s: 1, r: 0.8)
  mul = Mul(sources: @[env.UG, saw1.UG])
  mix = Mix(sources: @[mul.UG], amp: 0.2)

 ちょっとバリバリとした、素のノコギリ波とは違った音が鳴るようになりました。

 このように、複数の信号を影響させることで単純な波形からより複雑な音を得ることができるのです。

ステップ4: リズムに合わせて音を鳴らす

 リズムに合わせて音を鳴らすとはどういうことでしょうか。オシレータは常に音を発しつづけていますから、音を鳴らしたり止めたりということを表すユニットを別途実装する必要がありそうです。音の再生や停止は、オシレータの音の音量をコントロールすることでもあります。プログラムの中で時間をカウントしておいて、時間が音を鳴らすタイミングになったら音量を上げ、音を停止するタイミングになったら音量を下げることで音のオンオフを実現できます。
 音量変化については別の側面もあります。
 音量変化がゆっくりだとやわらかい音に、音量変化が速いと鋭い音になります。音色というのは周波数の分布だけでなく音量変化にも依存しているのです。音量の変化のみを表す0から1までの信号を用意してMulユニットでオシレータの出力に掛け合わせれば実現できそうです。

 整理します。リズムに合わせて音を鳴らすには次の3つを導入・実装しておく必要があります。

  • 時間の概念
  • 音量変化を表す信号
  • イベントキューとスケジューリング

 順番に見ていきましょう。

時間の概念

 これから時間に関する諸々を設計するのでまずは時間の概念をプログラムに導入します。

# https://github.com/t-sin/altair/commit/4300fbd16af07d307ae6a0ab2fe354921e8f287c
    Signal* = tuple[left: float32, right: float32]
    MasterInfo* = ref object
      sampleRate*: float32
+     tick*: uint64

  proc `*`*(s: Signal, v: float32): Signal =
    (s.left * v, s.right * v)

 このコミットで、プログラム全体の情報の中に「プログラム開始時から経過したサンプル数」をtickという名前で追加しました。サンプリング周波数は音を鳴らしている間は変えられないため時間の指標に用いることができます。1サンプル経過すると1/44100秒が経過する、というわけです。
 あとはPortAudioのコールバック関数内のサンプル毎の処理でこの変数をインクリメントしてやれば、その時点の時間を知ることができるようになります。以下はサンプル毎にPortAudioに渡すコールバック関数のコードです。この中で2つ後の節ででてくるイベント処理(シーケンサ)の更新と出力サンプル値の計算、時間情報の更新を行っています。

# https://github.com/t-sin/altair/commit/555b005a01596011a7718f23ca053ae018c17e60
# 開発時のものなのでもうシーケンサが登場してますが最後に説明します
    for i in 0..<framesPerBuf.int:
-     outBuf[i] = procUG(soundsystem.rootUG, soundsystem.masterInfo)
+     for ev in soundsystem.events:
+       procSeq(ev, soundsystem.mi)

+     outBuf[i] = procUG(soundsystem.rootUG, soundsystem.mi)

+     soundsystem.mi.tick += 1
+     soundsystem.mi.sec += 1.0 / soundsystem.mi.sampleRate

    scrContinue.cint

 soundsystemは自前のユニットジェネレータ達やイベントスケジューラを持っているオブジェクトです。mimasterInfoというオブジェクトで、Altairのサンプリング周波数等再生時の情報や時間進行情報を格納するオブジェクトです*5

音量変化を表す信号

 リズムに合わせて音を鳴らすには音のオン/オフの概念を導入する必要があります。音符を鳴らすタイミングでオシレータの音量を上げ、音符が終わったタイミングで音量を下げるのです。この音のオン/オフを一般化したADSRエンベロープという以下の図のような音量制御モデルがよく使われています。

f:id:t-sin:20200302111019p:plain
ADSRエンベロープ

 ADSRとはAttack, Decay, Sustain *6 , Releaseの略で、音の立ち上がり時間 (attack)、最大値からの減衰時間 (decay)、持続音の音量 (sustain) と0までの減衰時間 (release)の4つのパラメータで音量を制御するものです。たとえばアタックが短いと鋭い音になり、アタックが長くなると音がやわらかくなります。

 ADSRを実装するために以下のような構造体を用意します。

# 覚え間違いにより実際のソースコードでは以下の2つのスペルミスがある
# - SustainをSustinと書いてしまっている
# - elapsedをeplacedと書いてしまっている
type
  ADSR* = enum
    None, Attack, Decay, Sustin, Release
  Env* = ref object of UG
    adsr*: ADSR
    eplaced*: uint64
    a*: float32
    d*: float32
    s*: float64
    r*: float32

 adsrはよいとして、adsrは現在どの状態にあるかを保持しています。またelapsedは音符のオンになってからの時間です。

 これらをどう処理するかというと以下のように分岐の力技でゴリっと処理します。Envは外部からadsrの状態とelapsedを更新されると、自身の状態を更新しながら別の信号に掛けられる想定の(つまり音量を変化させる)信号——0から1までの値——を出力します。adsrの各状態によって分岐させ、その中でさきほどの図にあったようなある時間における音量値を決定して返します。

# 覚え間違いにより実際のソースコードでは以下の2つのスペルミスがある
# - SustainをSustinと書いてしまっている
# - elapsedをeplacedと書いてしまっているmethod procUG*(ug: Env, mi: MasterInfo): Signal =
  var
    a: uint64 = (ug.a * mi.sampleRate).uint64
    d: uint64 = (ug.d * mi.sampleRate).uint64
    s: float32 = ug.s
    r: uint64 = (ug.r * mi.sampleRate).uint64
    v: float32

  if ug.adsr == Attack:
    if ug.eplaced < a:
      v = ug.eplaced.float32 / a.float32

    elif ug.eplaced < a + d:
      v = 1.0 - (1.0 - s) * ((ug.eplaced - a).float32 / d.float32)
      ug.adsr = Decay

    else:
      v = ug.eplaced.float32 / a.float32
      ug.adsr = Decay

  elif ug.adsr == Decay:
    if ug.eplaced < a + d:
      v = 1.0 - (1.0 - s) * ((ug.eplaced - a).float32 / d.float32)

    elif ug.eplaced >= a + d:
      v = s
      ug.adsr = Sustin

    else:
      v = 0.0
      ug.adsr = None

  elif ug.adsr == Sustin:
    v = s

  elif ug.adsr == Release:
    if ug.eplaced < r:
      v = s - ug.eplaced.float32 * (s / r.float32)
    else:
      v = 0.0
      ug.adsr = None

  else:  # None
    v = 0.0f32

  ug.eplaced += 1
  (v, v)

 このエンベロープを用いて以下のようにすると0.1秒かけて音が立ち上がります。音符のオフを入力していないため音はそのまま鳴り続けます。

var
  saw1 = Saw(phase: 0, freq: 440)
  env = Env(adsr: Release, eplaced: 0, a: 0.1, d: 0, s: 1, r: 0.8)
  mul = Mul(sources: @[env.UG, saw1.UG])
  mix = Mix(sources: @[mul.UG], amp: 0.2)

 自動で音符オフをする処理も含め、譜面情報に従って音をオンオフする処理を次の節で実装します。

イベントのスケジューリング

 次に、事前に定義された譜面データにしたがってオシレータの音程とエンベロープジェネレータの内部状態を更新していく処理を定義します。まずはデータ構造を考えましょう。シーケンサーの導入です。

type
  Note* = tuple[freq: float32, sec: float, adsr: ADSR]

type
  EV* = ref object of RootObj

type
  Seq* = ref object of EV
    osc*: Osc
    env*: Env
    pat*: seq[Note]
    idx: int

 シーケンサーは入力としてオシレータとエンベロープジェネレータを受け取ります。エンベロープジェネレータの出力 (0から1)とオシレータの出力 (-1から1)を掛け算し音量制御とします。エンベロープジェネレータの内部状態の更新に用いるのがpatidxです。patNoteというイベントのリストです。Noteは周波数と時間とADSRの状態で表わされるオブジェクトです。再生中に現在の時間がpat内の時間を超えるとNoteイベントが処理され、そうでなければNoteイベントの時間がくるまで待つ(イベントを処理しない)というわけです。

 ここで時間の概念を導入したときに登場した、サウンドバッファを埋めていくコードをもういちど見てみましょう。

# https://github.com/t-sin/altair/commit/555b005a01596011a7718f23ca053ae018c17e60
    for i in 0..<framesPerBuf.int:
-     outBuf[i] = procUG(soundsystem.rootUG, soundsystem.masterInfo)
+     for ev in soundsystem.events:
+       procSeq(ev, soundsystem.mi)

+     outBuf[i] = procUG(soundsystem.rootUG, soundsystem.mi)

+     soundsystem.mi.tick += 1
+     soundsystem.mi.sec += 1.0 / soundsystem.mi.sampleRate

    scrContinue.cint

 soundsystem.eventsというのが2つ上のコードで導入したEV型です。EV型はイベント処理系オブジェクトのルートオブジェクトですが、その子の一つとしてシーケンサオブジェクトSeqがあります。Seq型のseqというスロットには譜面パターンがNoteの配列として保持されており、このうち何処にいるかを表すidxを更新する処理がprocSeqなわけです。以下のコミット

github.com

ではシーケンサのためのオブジェクトと補助関数を定義しています。だいたい音符の長さ(四分音符とか)からイベントを処理する時刻を計算する関数とか、音名(cとかdとか)から周波数を計算する関数とか、そういう雑多な処理が定義されています。これで、事前に定義した譜面を用いて音を鳴らしたり止めたりすることが可能になります。

 というわけでこれら3つを結合しバグを取った結果、以下のようなコードが動くようになりました。

# https://github.com/t-sin/altair/tree/d6683509c27eaba3bc868599b4768f623f5a47ab
var
  saw1 = Saw(phase: 0, freq: 440)
  rnd = Rnd(phase: 0, freq: 0)

var
  env = Env(adsr: Release, eplaced: 0, a: 0, d: 0.04, s: 0.1, r: 0.2)
  rhythm = Seq(env: env, osc: rnd.Osc, pat: len_to_pos(120, @[2,-1,1,1,1,1,1,2,2,2,2]))

var
  mix1 = Mix(sources: @[saw1.UG, saw2.UG], amp: 1)
  mul = Mul(sources: @[env.UG, rnd.UG])
  mix2 = Mix(sources: @[mul.UG], amp: 0.2)

 動かしてみるとスーパーなノコギリ波がリズムに合わせて音が鳴ったり止まったりしているようすが確認できます。

ステップ5: ユニットの構成をテキスト言語で指定する

 ここまでくるとユニットジェネレータのパラメータをいろいろにいじることでわりといろんなことができるようになります。しかしなんというか、いまのままではプログラムをコンパイルし直さないと挙動を変えることができなくて非常に面倒です。せめてこのユニットジェネレータの状態をユーザインターフェースかなにか、あるいはテキストファイルとかでDSLで指定できれば…。そんなわけでここではDSLとしてForthっぽい言語を設計し実装してみました、というのがこちらです。

github.com

 名付けてTapir Forthです!!
 Forthというのはスタック指向プログラミング言語あるいは連鎖性プログラミング言語 (concatenative programming language)として有名なあのForthですが、それっぽい言語を実装して、内部のユニットジェネレータを指定できるようにしました。これにより、以下のコードでドレミファソラシドを鳴らすことができす。

440 saw dup .s
0 0 1 0.1 adsr dup .s
rot swap .s

% push sequence pattern and make sequencer (not `rseq`)
() :c 3 3 n :d 3 3 n :e 3 3 n :f 3 3 n :g 3 3 n :a 3 3 n :b 3 3 n :c 4 3 n
seq () swap append .s

ev
() swap append swap append .s mul
ug

まとめ

 ちょっと駆け足でしたが音プログラムの設計から実装の流れをざっくりと解説してみました。この程度の簡単なもの(低レベルIOをしていない、機能をとても絞っている)ならわりとささっと実装できてしまうので、みなさん春休みの自由研究として音プログラムづくりに挑戦してみてはいかがでしょうか。

 ちなみに最近、ここ一年くらいつくっていたRust製音楽ソフトウェアKotoが完成したのでついでに宣伝しておきます。ファイルシステムをインターフェースとする、ライブパフォーマンス環境です。

github.com

*1:このあたりはWikipediaの「パルス符号変調」の記事などを参照のこと。

*2:英語版Wikipediaの"Unit generator"の記事を参照。

*3:時報のピッピッピッポーンはピッの部分が440Hzのラの音でポーンの部分は880Hzの1オクターブ高いラの音。

*4:ちなみにぼくはリング変調と振幅変調の違いがよくわかりません。前者はハードウェア的な観点で、後者は理論的な視座に立ったて見たときのもの…?

*5:DAWとかのコードを後で読んだところ曲の進行状況オブジェクトにはtransportという名前をつけるのが一般的だったっぽい

*6:ちなみに今日2020-03-05までsustinだと思っていました