octahedron

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

退職します

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

 現職に入社したのは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だと思っていました

tanasinnをぼくの手の上に

tanasinnをぼくの手の上に

 この記事ではGoを使ってコマンドラインプログラムを書いたよーという話をします。

tanasinnコマンドがほしい

 ところでみなさん、tanasinnを知っていますか? tanasinnというのはインターネットミームのひとつです。2ちゃんねる掲示板に投稿されたのが初めてで、どこか見ていると不安になってくるようなシュールなアスキーアート群についた名称です。ニコニコ大百科の当該記事によれば、以下のようなものです。

  • 退廃的で人にどこ∵∴∵な(○)不安を感じさせる雰囲気。
  • 侵蝕に対する人類意識の根源的な恐怖感・畏∵(●)∴∵徴。
  • インターネット内に存在する別世界・別精神で∵り、全世界の根幹を成し操作している。我々はtanasinnに常に見られている。tanasinn(●)決して怖くな∵∴         (●)
  • Don't think. Feel and you'll be tanasinn.

 この不安になる感じがとてもよいですね。

 ぼくもtanasinnになりたい!!!!!

 そこで手軽にtanasinnになれるコマンドをGoの勉強がてら作成してみました。

tanasinnコマンド

 まずはtanasinnコマンドをどのように作るか考えてみます。
 利用ケースとしてはSlackなどで発言するときなどが考えられますが、後々なんにでも使えるようなデザインにしておきたいです。そこでtanasinnコマンドはシェルコマンドのフィルタプログラムのように実装することにしました。引数でファイルを受け取るかあるいはパイプされた文字列の文字をランダムにtanasinn化して吐き出すプログラムとして実装します。tanasinn化度合いを引数で指定できるといいかもしれません。

いかにtanasinnするか

 さて、文字列をtanasinn化する方法を考えるのですが、文章におけるtanasinnっぽさを突き詰めると隠れマルコフモデルだとかなんだとか、ちょっとよく知らない概念あるいは宇宙の真理に手を出さなければならなくなりそうです。ここでは簡単に、入力文字列中の文字がランダムに所与のtanasinn文字列に置き換えることでtanasinn化します。つまりPythonっぽい疑似コードで書けばこうです:

for 文字 in 入力文字列:
    n = 0.0から1.0乱数を生成

    if nが閾値を超えていたら:
        tanasinn文字列をランダムにピックアップして出力
        
    else:
        文字を出力

 ここでまず上記コードのfor文の中身を行う補助関数を用意します。

func tanasinnize(th float64, r rune) string {
    s := []string{
        "∴",
        "∵",
        "∴∵",
        ")",
        "(・)",
        "(・",
        "∴・",
        "∴",
        "・・",
        "・:",
        "..",
    }
    if th < rand.Float64() {
        return fmt.Sprintf("%s", s[int(rand.Uint32()) % len(s)])
    } else {
        return fmt.Sprintf("%c", r)
    }
}

 tanasinn化文字列は関数内の定数として保持しています。生成した乱数がもし引数で与えられた閾値を超えていたらtanasinn化文字列を出力し、そうでなければ入力された文字を返すといういたってシンプルなものです。

 あとはこれを文字列内の文字についてのループで呼び出してやり、標準入力まわりを整えてやれば完成です。

 できたものがこちらになります。

github.com

 動かすとこのようになります。

$ echo "Don't think. Feel and you'll be tanasinn." | ./tanasinn
(・on't th..nk. ・:eel ∵nd y∴u'l∴ be ta∴∴sinn.

 これで気軽にtanasinn化できるようになりました。

だんだんtan(・sinn∴なっ・・い∴∵..・:

 しかしあまりにシンプルすぎてちょっと面白くないです。具体的には「あぁ……消えて…い…k……」みたいなことがしたいです。なのでtanasinn閾値を引数で弄れるようにし、さらに閾値区間指定できるようにします。

 コマンドライン引数-tに数値(float64)を指定するとそれが閾値として採用されます。また、閾値1.0,0.6,0.3,0.1のようにカンマ区切りで渡すと、入力文字列を区間数で等分した領域の閾値を前から指定できます。たとえば閾値1,0のとき、区間数は2なので前50%が閾値1、後ろ50%が閾値0としてtanasinn化されます。1.0,0.6,0.3,0.1のときは最初の25%が1.0、次の25%が0.6、……という感じです。

 この文字列をパースして閾値のスライスを取り出す処理をparseThreasholdsという補助関数にしました。ループや分岐、スライスの扱い、エラーハンドリング等の基本的なGoの要素が登場し、入門に最適な感じです。

func parseThreasholds(th string) ([]float64, error) {
    var ths []float64

    if strings.Contains(th, ",") {
        list := strings.Split(th, ",")
        for i := 0; i < len(list); i++ {

            f, err := strconv.ParseFloat(list[i], 32)
            if err != nil {
                log.Fatal(err)
                return ths, &parseError{s: th, reason: "not float in the list"}
            }

            ths = append(ths, f)
        }

    } else {
        f, err := strconv.ParseFloat(th, 32)
        if err != nil {
            log.Fatal(err)
            return []float64{}, &parseError{s: th, reason: "not float"}
        }
        ths = []float64{f}
    }

    return ths, nil
}

 こいつを引数のパース処理に組込み、標準入力の内容をいったんメモリに全て読み込むように変更し(区間指定するには全体の流さが必要だからです)、あとは文字についてのループを以下のように変更します。

 for n := 0; n < len(text); n++ {
        p := float64(n) / float64(len(text))
        i := int(p * float64(len(option.th)))
        r := text[n]
        if r == '\n' {
            fmt.Printf("\n")
        } else {
            fmt.Printf("%s", tanasinnize(option.th[i], r))
        }
    }
}

 pは入力文字列中の今の位置の割合です。それをoption.thという閾値スライスの長さを掛けて小数点以下を切り捨てることで、閾値スライスの中での位置を計算しています。

 ということで実装したものがこちらのリポジトリです。

github.com

使ってみるとこのような感じです。

# 閾値0.2
$ echo "だんだんtanasinnになっていく……" | ./tanasinn -t .2
)∴∵・・..・:an∴・・:・:∴・∴・:∴・()∴・・・・・…∴・

# 閾値0.7
$ echo "だんだんtanasinnになっていく……" | ./tanasinn -t .7
∴・∴だんtanas..・・nに)()ていく……

# だんだんtanasinn化する例
$ echo "だんだんtanasinnになっていく……" | ./tanasinn -t 1,0.6,.4,.2
だんだんta∴(()()nnにな・:)..く..…

おわりに

 この記事ではさくっと作ったtanasinn化コマンドの紹介と中身の解説をしました。やってることは単純ですがこれで気軽にtanasinnごっこができるようになりました。

悲しみの記

今回はとっても愚痴記事です。読まないことをお勧めします。

ぼくがやりたいことはいくつかあって(一般にはたくさんと呼ばれるらしい)、それを実現するために動きたいと常日頃考えている。先日はやりたいことのひとつである「リアルタイム操作可能なシンセサイザープログラム」のミニマムかつ最初の成果物であるKoto https://github.com/t-sin/koto とかリリースした。

この先にあるのはRez InfiniteやDEPTHといった音楽ゲームの音楽演奏部分の基礎理解と私的モジュール化だ。

やりたいことの他にはグラフィクスプログラミングやプレイヤーの体験を設計するという意味でのゲーム実装、プログラミング言語処理系への理解の強化などある(よく誤解されるが「言語処理系」といったとき「形式言語を処理するシステム」の意味で用いているので注意)。もともとチューリングマシンかっけーからプログラミングに入ってきたので、低レイヤーとても興味があるのだ。

ところで、人生の最も多くの時間を割かざるを得ない活動がある。労働である。ヒトの仕事を機械が完全に奪ってくれていない現在において、日銭あるいは趣味のための資金を捻出するのに労働は欠かせない。

しかし労働は大抵の部分において苦である。

電車通勤を強いられたりオフィス移転により通勤距離の変更(遠くなったり乗り換えめんどくさくなったりするかもしれない)。私的にやりたいことが山ほどある人間にとっては1日8時間以上を当然の顔して掠め取られるだけでも大打撃だ。内容だって苦しみが多く、まずもってやりたいこと・伸ばしたいことと業務が一致することは多くない。会社の方針、理解されない提案、理解しがたい提案、開発陣と経営陣の連携のなさ、マイナー言語好きのつらみ、孤独感。要するに虚無である。

では環境を変え、なんならやりたいことに近い分野の仕事を探せばよいのでは。と転職活動もやってみた。

しかし、事業内容に興味を持てないのである。世の中を技術で変えることは大事かもしれないが、ぼくにとっての気持ちよさはかけらもない。ぼくにとっての気持ちよさとは、やりたいことを要約して、気持ち良いものをつくれることにあるからだ。そして、そのような仕事がないか探してみた。応募もしてみた(たしかにゲーム系請負の小さな会社だった)。しかし、ぼくにフィットする会社がみつからないのである。あるいは待遇がとても悪く、ビビったりした。こちらがまだ勉強不足である面は看過しがたい。しかし労働に身を縛られているため勉強にさける時間は全く多くはない。詰んでいる。

また、やりたいことに重なるようなことがネットで世の中で既にやられていくのを見た時の無力感といったらないのだ。

やりたいことがたくさんある故、ふとこのように自問することがある。 「このままではなにもわからない人間でただ終わるのではないか」

ぼくはそのように終わるのは嫌だが、何かを成し遂げられるという希望が定期的に潰えては怯えている。

ぼくは、どうなってしまうんだ?

C言語製の自由なDAW Zrythmをビルドしてみる

あらまし

 最近GitHubを眺めていたらオープンソースDAW (Degital Audio Workstation; つまり作曲ソフト)を発見しました。
 そんなDAWの名はZrythm。

www.zrythm.org

 そのDAWは、

  • ほとんどC言語で書かれており、
  • 開発を開始したのが2018年7月からのようであり(2年目くらい)、
  • AGPLでライセンスされており、
  • 何故か作者が日本語ローカライズをしており、
  • なぜか名前がZrhythmではなくZrythmであり、
  • WindowsGNU/Linuxで動く

というものでした。新興のDAWでありながら公式のスクリーンショットとかを見るかぎりよくできていそうで、1年ちょっとでこのレベルのものが生えてくるのねとヴィックリ。

 この記事ではそんなZrythmをビルドして動かしてみました。

ビルド環境

 Zrythmではビルドツールとしてninja、Ninjaのビルドファイル生成ツール(つまりCMake的なツール)としてMesonを利用しています。Ninjaはmakeよりも小さくて速いビルドツールとして開発されているもので、かつNinjaのビルドファイルは自動生成される想定で書かれているため別途Mesonが必要という感じらしいです。ちなみにちょっと前にFUSEのコードをみたらMesonとNinjaを使っていたので、最近のC界隈のトレンドのようです。

 Ninjaはコードをクローンしてきて./configure.py --bootstrapで、MesonはPython製なのでpython3 -m venv venv ; . venv/bin/activate; pip install mesonとかでセットアップしてください。

 手元のビルド環境を晒しておくとこんなかんじです。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -a
Linux hogehoge 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:24:28 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ meson --version
0.53.1
$ $HOME/tmp/ninja/ninja --version
1.10.0

依存ライブラリのインストール

 まずは依存ライブラリを入れていきます。以下のライブラリのヘッダが必要なので入れていってください。

  • libsndfile (音声ファイルの読み書きライブラリ)
  • librubberband (音声データのリピッチ・ストレッチライブラリ)
  • libsamplerate (音声データのサンプリングレート変換ライブラリ)
  • libyaml (YAMLライブラリ)
  • libfftw3 (高速フーリエ変換ライブラリ)
  • libgtk-3 (GTK3)

 それにしてもsndfileは知ってたんですが(cl-sndfileとかある)、リピッチとかサンプリングレート変換とかFFTもライブラリがあるんですね。

Zrythmのビルド

 依存ライブラリを入れおわったら、Zrythm本体のコードをもってきます。GitHubに公式でミラーされているのでこちらを持ってきましょう。公式サイトに書いてあるGitリポジトリはなんだかモジュールごとにリポジトリが切ってありサクッとビルドしたい方にはオススメしません。

github.com

 Mesonが使える状態の元、このようにするとビルドできます(ninjaをシステムにインストールしたくなかったため、直接パスごと指定してます)。

$ git clone https://github.com/zrythm/zrythm
$ cd zrythm

# mesonを叩くときにはninjaにパスが通っている必要がある
$ PATH=$PATH:$HOME/tmp/ninja/ meson build
$ PATH=$PATH:$HOME/tmp/ninja/ ninja -C build

# インストール
$ sudo $HOME/tmp/ninja/ninja -C build install

 これでzrythmが/usr/local/binあたりに置かれ、起動できるようになります。

使用感

 さて、使ってみるタイムです。
 ちなみにデフォルトのビルド設定ではサウンドドライバがJackのみでしたが起動時に再生中の音楽が止まるのがいやだったのでPortAudioオプションを有効にしてビルドしています。

f:id:t-sin:20200201015841p:plain
Zrythm起動時の画面

 …なんですが。

 まず、サウンドドライバ設定でPortAudio選べないのね! しかたがないのでjack_control startでJackを起動しました。すると、上のスクショにも出てますがXRUN occuredとでたまま固まってしまいました。どうもJackを起動した状態で繋ぐと死ぬっぽい。バージョン合ってない系かしら(いやしかしjack_lspでポート一覧にはでてくる)…。

 コードはかなり素直で読みやすい(あとCでOOPっぽさのあるコードになっている)ので、ちょっと調べてみるといろいろ楽しそうですねえ。