V3での変更

mimium v3における言語仕様の変更 #

音楽のためのプログラミング言語mimium v3は言語仕様とランタイムに大幅なアップデートを含みます。言語仕様そのものに後方互換を破る変更はありませんが、一部の組み込み関数とライブラリに破壊的変更が導入されます。

主な変更点には、レコード型の導入、それに付随した柔軟な関数呼び出しのためのパラメータパック、多段階計算(型安全マクロ/コンパイル時計算)、ライブコーディングサポートが含まれます。

非破壊的な変更(新機能) #

レコード型と構文 #

基本的なレコード型 (#99, #128) #

mimium v3では、Elmに着想を得た構文を持つレコード型(他言語の構造体に相当)を導入しています。レコードを使うことで、名前付きフィールドで関連するデータをグループ化でき、コードの可読性と保守性が向上します。

基本構文:

// 名前付きフィールドを持つレコードリテラル
let myadsr_param = { 
   attack = 100.0,
   decay = 200.0,
   sustain = 0.6,
   release = 2000.0,
}

// 型注釈はオプション
let myrecord = {
  freq:float = 440.0,
  amp = 0.5,
}

// 単一フィールドのレコードには末尾のカンマが必要
let singlerecord = {
  value = 100,
}

レコードフィールドへのアクセス:

// ドット演算子によるフィールドアクセス
let attack_time = myadsr_param.attack

// let束縛でのパターンマッチング
let {attack, decay, sustain, release} = myadsr_param

// アンダースコアを使った部分適用と組み合わせ
let myattack = myadsr_param |> _.attack

レコード更新構文 (#158):

レコードの変更版を作成することは関数型プログラミングでは一般的なパターンです。mimium v3では、レコードを更新するためのクリーンな構文を導入しています:

let myadsr = { attack = 0.0, decay = 10.0, sustain = 0.7, release = 10.0 }

// 特定のフィールドを更新し、他は変更しない
let newadsr = { myadsr <- attack = 4000.0, decay = 2000.0 }
// newadsr は { attack = 4000.0, decay = 10.0, sustain = 0.7, release = 10.0 }

// 元のレコードは変更されない(イミュータブルなセマンティクス)
// myadsr はまだ { attack = 0.0, decay = 10.0, sustain = 0.7, release = 10.0 }

レコード更新構文はシンタックスシュガーとして実装されており、関数型プログラミングのセマンティクスを保証します。既存のものを変更するのではなく、新しいレコードを作成します。

パラメータパックとデフォルト引数 #

パラメータパック (#130) #

パラメータパックを使うと、関数がタプルやレコードを引数として受け取り、自動的に個別のパラメータにアンパックできます。この機能は、複数のパラメータを持つ関数の使いやすさを大幅に向上させます。

タプルの場合:

fn add(a:float, b:float)->float {
  a + b
}

// 個別の引数での直接呼び出し
add(100, 200)  // 300を返す

// タプルの自動アンパック
add((100, 200))  // 300を返す

// パイプ演算子とシームレスに連携
(100, 200) |> add  // 300を返す

レコードの場合:

fn adsr(attack:float, decay:float, sustain:float, release:float)->float {
  // ADSRエンベロープの実装...
}

// レコードで呼び出し - フィールドの順序は任意
let params = { attack = 100, decay = 200, sustain = 0.7, release = 1000 }
adsr(params)

// またはインライン
adsr({ decay = 200, attack = 100, release = 1000, sustain = 0.7 })

この機能は、多くのパラメータを持つことが多いオーディオ処理関数で特に有用であり、明確性のためにパラメータをレコードとして整理できます。

デフォルト引数 (#134) #

関数でパラメータのデフォルト値を指定できるようになり、一般的な設定で関数を呼び出す際のボイラープレートが減少します:

fn foo(x = 100, y = 200) {
  x + y + 1
}

fn bar(x = 100, y) {
  x + y
}

fn dsp() {
  // 空のレコード{}を使ってすべてのデフォルトを受け入れる
  foo({}) +           // x=100, y=200を使用、301を返す
  bar({y = 300})      // x=100, y=300を使用、400を返す
  // 合計: 701
}

デフォルト値はパラメータパック構文と連携します。{}を使ってすべてのデフォルト値を受け入れ、上書きしたいパラメータのみを指定できます。現在、これは直接的な関数呼び出し(クロージャや高階関数でないもの)でのみ動作します。

多段階計算(型安全マクロ/コンパイル時計算) (#135) #

mimium v3の最も強力な追加機能の1つは、MetaOCamlやScala 3のマクロ、SATySFiなどに影響を得た多段階計算です。これにより、型安全性を維持しながら、コンパイル時にコードを生成するマクロを書くことができます。

動機 #

オーディオ処理には、しばしば2つの異なるフェーズが含まれます。

  1. 計算グラフの構築(構造)
  2. 計算グラフの実行(サンプル処理)

以前は、パラメトリックなオーディオグラフには高階関数をグローバル環境で実行してから使用するというステップを挟む必要がありました。多段階計算により、これをより自然で効率的に記述できます。

基本構文 #

多段階計算は、計算ステージを0(macro)1(main)に分け、それぞれのステージでの計算を行き来するための構文と、ステージ0から見て次のステージで評価される型(Code型)が型システムに追加されます。この方法により、マクロとは言いつつも、計算体系自体はソースコードや構文木を操作するのではなく、直接ランタイムで走るコードと共通したラムダ計算に基づくものにできます。マクロ展開前に型検査と推論が完了することが最も大きな特徴です。

2つのプリミティブがマルチステージ計算を可能にします。クォート (`expr) は次のステージで評価される式をマークします。スプライス ($expr) は前のステージから式を評価して挿入します。マクロ呼び出し構文 (f!(args)) はRustのマクロ呼び出しに似ていますが、mimiumでは ${f(args)} のシンタックスシュガーとして実装されます。。

例 - パラメトリックなフィルターバンク:

元々の、高階関数を使ったパラメトリックな信号処理グラフ生成では、グローバル環境で一度高階関数を実行してから使用する必要がありました。 これは、高階関数が実行されるタイミングで初めて信号処理用のメモリが確保されるため、毎サンプル新しいプロセッサを生成する意味になっているからです。

fn bandpass(x,freq){
      //...
    }
fn filterbank(n,filter_factory:()->(float,float)->float){
  if (n>0){
    let filter = filter_factory() 
    let next = filterbank(n-1,filter_factory)
    |x,freq| filter(x,freq+n*100)
             + next(x,freq)
  }else{
    |x,freq| 0
  }
}
let myfilter = filterbank(3,| | bandpass)//ここで一度グラフ生成
fn dsp(){
      myfilter(x,1000)
}

マクロを用いると、定義された高階プロセッサをdsp関数の中に直接埋め込むことができます。

#stage(main)
fn bandpass(x, freq) {
  // バンドパスフィルタの実装...
}
#stage(macro)
fn filterbank(n, filter:`(float,float)->float) -> `(float,float)->float {
  if (n > 0) {
    `{ |x,freq| 
       filter(x, freq + n*100) + filterbank!(n-1, filter)(x, freq)
    }
  } else {
    `{ |x,freq| 0 }
  }
}
#stage(main)
fn dsp() {
  filterbank!(3, `bandpass)(input, 1000)
  // コンパイル時に展開されたフィルターバンクを生成
}

グローバルステージ宣言構文 (#154) #

変数のスコープを維持しながらステージを行き来していると、クオートとスプライスのネストがどんどん深くなっていきます。

深くネストされた括弧を避けるため、mimium v3ではグローバルレベルのステージ宣言を導入しています。

#stage(macro)// ここ以下のすべてはコンパイル時(ステージ0)で評価される
fn mymacro(n:float) -> `float {
  `{ $n * 2.0 }
}
let compile_time_constant = 42

#stage(main)// ここ以下すべては実行時(ステージ1)で評価される
fn dsp() {
  mymacro!(21)  // 結果は42.0
}

ライブステート更新 (#159) #

mimium v3では、オーディオを途切れさせることなく実行中にコードを変更できる画期的なライブコーディング機能を導入しています。

仕組み #

多段階計算の導入により、ほとんどの信号処理での必要なメモリレイアウトはコンパイル時に決定できるようになりました。dsp関数からの関数コール、ディレイ呼び出し、selfの使用などを静的解析することにより、ソースコードに変更を加えてもディレイやフィードバックの内部状態を可能な限り保つように動作します。

新しいmimium-cli変更されたmimiumファイルを上書き保存すると、ファイル変更を自動検知して以下のステップでDSPを更新します。

  1. コンパイラがバックグラウンドでコードを再コンパイル
  2. 新しいVMが実行中のVMと比較される
  3. 内部状態(ディレイ、フィードバックなど)のツリーを作成し、構造で変化がない部分を抽出して新しいVMの内部状態メモリを作成
  4. オーディオスレッドが新しいコードにシームレスに切り替わる

このシステムはいくつかのメリットを提供します。まず、オーディオの中断がなく、ディレイのテールやリバーブが自然に続きます。単に定数を変更した場合など、内部状態ツリー構造に変化がない場合は単にメモリを丸ごとコピーするだけでよいなど、全ての構文木を比較する必要がないのも特徴的なポイントです。さらに、多段階計算のfilterbankの例のような、マクロへの引数でフィルタの次数を変えるような、コンパイル時定数の編集によるオーディオグラフの変化にさえ自然に追従できます。

このような、ライブコーディング的な機能ではありつつも、毎回ソースコードを丸ごとコンパイルし直して新しいバイトコードとVMインスタンスを生成しつつ音を途切れさせないという、独特の方式をとっています。

この機能追加により、mimiumは非常に低レイヤーなDSPのコーディングを聴感上違和感なくライブコーディングすることができる、数ある音楽プログラミング言語の中でも唯一無二といっても良い機能を獲得しました。

使用例:

// 初期コード
fn dsp() {
  let freq = 440.0
  sin(phasor(freq) * 3.14159 * 2.0)
}

// 実行中に周波数を変更できる:
fn dsp() {
  let freq = 880.0  // 編集して保存するだけ - 再起動不要、モジュレーションなどさらなる内部状態を持つものでもOK
  sin(phasor(freq) * 3.14159 * 2.0)
}

新しいオシレータの追加、フィルタパラメータの変更、ディレイタイムの変更、オーディオグラフの再構築など、さらに複雑な変更も機能します。

実装は、決定論的な状態位置のための静的メモリ割り当てを持つ状態管理用のstate_treeクレートを使用します。最大効率のための最小限のデータコピーで、コールツリー内の挿入、削除、置換を検出します。

Language Server (#151) #

mimium v3には、より良い開発者体験のためのIDE機能を提供する新しい言語サーバープロトコル(LSP)実装が含まれています。

現在の言語サーバーは、コンパイラのトークナイザに基づく適切なシンタックスハイライトと、タイピング中のリアルタイムエラーチェックとエラー報告機能が実装されています。。VSCode統合はmimium VSCode拡張v2.3+で動作します。

実装はtower-lspフレームワーク上に構築され、mimiumの既存のコンパイラインフラストラクチャを再利用します。オートコンプリートやgo-to-definitionなどは将来的に実装される予定です。

GUIとツーリングの改善 #

スライダーUI (#147) #

新しいSlider!マクロは、オーディオパラメータ用の使いやすいGUIコントロールを提供します:

fn dsp() {
    let gain = Slider!("gain", 0.5, 0.0, 1.0)  // 名前、デフォルト、最小、最大
    let freq = Slider!("frequency", 440.0, 20.0, 20000.0)
    
    sin(phasor(freq) * 3.14159 * 2.0) * gain
}

スライダーの値はリアルタイムで利用可能であり、オーディオの実行中に調整できます。

コードフォーマッター (#143) #

実験的なコードフォーマッター(mimium-fmt)が追加されました:

mimium-fmt myfile.mmm

フォーマッターはAST基づく一貫したコードフォーマットを提供し、レイアウトにprettyクレートを使用します。更新されたパーサー(chumsky v0.10.x)と統合されています。ただし、triviaがまだASTに保存されていないため、フォーマット中にコメントが削除されます。したがって、これは現時点では実験的と考えてください。

破壊的変更 #

オーディオファイル読み込み: gen_sampler_monoSampler_mono! (#161) #

ランタイムのgen_sampler_mono関数がコンパイル時のSampler_mono!マクロに置き換えられました。

旧構文:

let mysampler = gen_sampler_mono("audio_file.wav")
fn dsp() {
  mysampler(phase)
}

新構文:

fn dsp() {
  let phase = phasor(1.0)
  Sampler_mono!("audio_file.wav")(phase)
}

オーディオファイルはコンパイル時にロードされ、VMのデータセクションに直接埋め込まれ、ランタイムファイルI/Oなしで優れたパフォーマンスを提供します。システムには自動キャッシングとリソース管理が含まれており、ファイルは再コンパイル間でキャッシュされ再利用されます。

プローブ関数: make_probeProbe! (#156) #

make_probe関数は、他のGUI機能との一貫性のためにProbe!マクロに置き換えられました。

旧構文:

let myprobe = make_probe("test")
fn dsp() {
    osc(440) |> myprobe
}

新構文:

fn dsp() {
   osc(440) |> Probe!("test")
}

両方の変更は、より多くの作業をコンパイル時に移すためにマルチステージ計算を使用するというmimium v3の哲学に沿っており、より良いパフォーマンスとクリーンな構文をもたらします。