octahedron

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

音プログラムをつくる

 先日(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だと思っていました