overdub

schwung-deep-space

hiroikezawa/schwung-deep-space · ★ 0

Deep Space Convolution: convolution + FDN deep-space reverb audio FX for Schwung on Ableton Move

README from GitHub, MIT

Deep Space Convolution

Ableton Move 上の Schwung で動く、オリジナルの Audio FX モジュールです(id deep-space)。

IR(インパルス応答)による畳み込みで空間の初期反射と固有の響きを作り、その出力で 16 ラインの FDN(Feedback Delay Network)を励振して、最大 30 秒の後期残響へつなぎます。 目標はステレオワイドナーではなく、遥か遠くに広がる大聖堂・巨大洞窟・地下構造物・暗い無限空間の 「奥行き」です。

  • 8 ノブ 1 ページ(Schwung 標準のノブグリッド、ノブ 1 は MIX)。トラックのスロットでも Master FX でも使える
  • IR は 6 種類すべて合成 IR(固定シードから実行時に生成)。録音 IR・第三者の IR は含まない
  • DSP・FFT は完全オリジナル実装、MIT License(作者 Hiro Ikezawa)。外部ライブラリなし
  • 現在のバージョン:0.1.1
ノブパラメータ範囲初期値
1MIX0–100 %(等パワー)40 %
2SPACECATHEDRAL / CAVERN / TUNNEL / HANGAR / ABYSS / VOIDCATHEDRAL
3DEPTH0–100 %60 %
4SIZE0.50–4.00 x1.50 x
5DECAY1.0–30.0 秒8.0 秒
6DIFFUSION0–100 %70 %
7DARKNESS0–100 %40 %
8FREEZEOFF / ONOFF

初期値は「入力音をはっきり残したまま、背後に巨大な奥行きがある」状態です。

インストール

リリースから入れる(おすすめ)

  1. Releases から最新の deep-space-module.tar.gz をダウンロードします。

  2. Schwung Manager(http://<MoveのIP>:7700)の Modules →「Install custom」で、そのファイルをアップロードします。 コマンドラインからなら:

    python3 scripts/install.py http://<MoveのIP>:7700 --package deep-space-module.tar.gz
    
  3. Move の電源を一度入れ直します。 一度読み込まれた DSP は、スロットを読み直しても入れ替わりません。

動作確認は Schwung 1.7.3(Ableton Move)で行っています。

ソースからビルドして入れる

aarch64 用のクロスコンパイラ(gcc-aarch64-linux-gnu)と Python 3 が必要です。 Schwung の plugin API ヘッダは third_party/schwung/ に同梱しているので、Schwung のソースは不要です (UI グリッドのテストだけは Schwung のチェックアウトを使い、無ければスキップします)。

bash scripts/test.sh                                 # 全テスト(約 1 分)
bash scripts/build.sh arm64                          # dist/deep-space-module.tar.gz を作成
python3 scripts/install.py http://<MoveのIP>:7700    # Schwung Manager 経由でアップロード
bash scripts/render_demo.sh                          # 試聴用 WAV を build/demo/ に書き出し

使い方

  1. トラックのスロット(Signal Chain)の Audio FX、または Master FX(Shift+Vol+Menu)に Deep Space Convolution を入れる
  2. まず初期値のまま鳴らす。DEPTH を上げると音源そのものが遠くへ下がっていく
  3. SPACE で空間の種類、SIZE で空間の大きさ、DECAY で後期残響の長さを決める
  4. FREEZE を ON にすると、その瞬間の残響がそのまま持続し、以降の入力は空間に入らない(ドライ音はそのまま通る)

SPACE / SIZE を変えたとき、および DECAY を IR 自身の減衰より短くしたときは、新しい IR を オーディオスレッド上で少しずつ生成・変換し(約 0.2 秒)、準備ができた時点で約 0.3 秒かけて クロスフェードします。ノブを回している間は生成をやり直し続け、止まった値で 1 回だけ切り替わります。

各ノブの動作

  • SPACE:6 種類の合成ステレオ IR を切り替える(下表)。
  • DEPTH:単純な Wet 量ではなく「距離」です。次をすべて連動して動かします。
    • プリディレイ 3 → 180 ms(SIZE で 0.85–1.9 倍)
    • 初期反射 / 後期残響の比率(初期反射のゲイン 1.0 → 0.3、後期 0.5 → 1.3)
    • 遠方の空気吸収(Wet のローパス:オフ → 約 2.6 kHz)
    • 反射密度(初期反射と FDN 入力の拡散オールパスの係数が上がる)
    • 直接音 / 残響音の比率(直接音を最大 −12 dB、かつ高域を落とす。MIX が 0 のときは何もしない)
    • ラウドネス補償(Wet を最大 +4.1 dB)で、遠ざけても「ただ小さくなるだけ」にならないようにしている
  • SIZE:空間のスケール。IR の時間構造(初期反射の時刻、最初の反射までの間、ブルーム、フラッター周期、 IR の長さ、IR の減衰時間、金属的な共鳴の周波数)を作り直し、FDN の 16 本のディレイ長 (SIZE^0.75 倍)とプリディレイも伸縮します。ピッチ変更ではありません(FDN の長さは約 0.35 秒かけて 滑らかに追従するので、回している間だけわずかにドップラー的な揺れが出ます)。
  • DECAY:後期残響の RT60(低域)。FDN の各ラインの減衰をライン長ごとに計算しているので、 SIZE を変えても RT60 は変わりません。IR 自身の減衰が DECAY より長い場合は IR 側も短くします。
  • DIFFUSION:FDN 入力の拡散オールパス(4 段)、初期反射の密度、FDN のディレイ変調量。 低いと反射の粒が見え、高いと滑らかな壁になります。
  • DARKNESS:FDN の周波数依存減衰(高域の RT60 を低域の 0.9 倍 → 0.08 倍)と Wet の高域。
  • FREEZE:下記。
  • MIX:等パワーのドライ / ウェット。0 % は完全なバイパス(ビット一致)、100 % はドライなし。 MIX が 0 まで下がりきると(約 0.4 秒)残響の計算そのものを止め、CPU はほぼ 0 になります。 MIX を上げ直すと残響の履歴を消去(約 20 ms、その間は Wet 無音)してから空の空間で再開するので、 MIX 0 の間の入力は残響に残りません。FREEZE が ON のときは止めないので、MIX 0 → 上げ直しで 同じ凍結音が戻ります。MIX 0 の間に SPACE / SIZE を変えた場合も IR は作っておき、上げ直した時点で使います。

FREEZE の仕組み

ON にすると、空間への入力(プリディレイ前)を 0.25 秒、FDN への入力を 0.12 秒かけて遮断し、 同時に FDN の RT60 を 0.5 秒かけて 30000 秒へ移します。凍結中も FDN 入力を閉じているのは、 畳み込みがまだ鳴らしている IR の尾が、損失のなくなったループに溜まって音量が上がるのを防ぐためです。 RT60 30000 秒は 5 分で約 −0.6 dB の減衰で、このわずかな損失がループの安定性を保証しています (完全な無限持続ではなく、安定性を優先)。OFF で逆の順に戻り、通常の DECAY で減衰します。

IR プリセット(すべて合成)

各 IR は dsp/ds_irgen.c が固定シードから生成します。同じ設定なら、どのマシンでも毎回同じ IR になります。 モデル:共有ノイズと各チャンネル固有ノイズの混合(ステレオ幅)を、クロスオーバーで分けた 2 帯域の 指数減衰(高域が先に消える)× ブルーム × ゆっくりした振幅の揺らぎで成形し、初期反射のインパルス列、 平行壁のフラッターエコー、構造の共鳴(減衰する正弦波)を加えて、2 段ローパス + ハイパスで空間の 音色を付け、最後の 1/4 をフェードアウトします。エネルギーで正規化し、プリセット間の音量差を補正しています。

SPACE特徴(SIZE 1 のとき)
CATHEDRAL最初の反射 22 ms、30 本の初期反射(〜160 ms)、低域 RT 4.2 s / 高域 2.2 s、70 ms のブルーム、広いステレオ
CAVERN9 ms からの密集した不規則な反射 44 本(〜240 ms)、暗い(5.2 kHz)、低域 RT 4.8 s / 高域 1.3 s、大きな揺らぎ
TUNNEL41 ms 間隔のフラッターエコー、187/374/561 Hz の管の共鳴、狭いステレオ、低域を少し削る
HANGAR30–340 ms の大きく離れたスラップ 9 本、明るい(15 kHz、高域 RT 2.8 s)、6 本の金属的な共鳴
ABYSS初期反射なし、480 ms かけて闇から立ち上がる、極端に暗い(2.6 kHz)、超低域寄り、低域 RT 7.5 s
VOID初期反射なし、完全に無相関なステレオ、160 Hz 以下なし、明るく長い(高域 RT 4.5 s)、260 ms のフェードイン

IR の長さは 1.4–2.0 秒 × SIZE(上限 2.0 秒)。確認用に scripts/render_demo.sh が build/demo/irs/<SPACE>.wav に IR そのものを書き出します(SIZE 1.5、DECAY 30、ピーク −1 dBFS)。

DSP 信号経路

Stereo In ─┬───────────────────────────────── 直接音(DEPTH で距離感: 減衰 + 高域カット)─┐
           │                                                                                │
           └→ FREEZE ゲート → プリディレイ(DEPTH / SIZE)                                  │
                 → 分割畳み込み(ステレオ IR)                                              │
                 → 密度拡散(2 段オールパス) ─────────────┬──→ 初期反射 × gE(DEPTH)        │
                 → 入力拡散(4 段オールパス)→ FDN ゲート   │                               │
                 → 16 ライン FDN(Hadamard、周波数依存減衰、変調) → 後期残響 × gL(DEPTH)   │
                       ↓                                                                    │
                 空気吸収 LP(DEPTH・DARKNESS)→ ラウドネス補償 → DC 除去 → Wet ピーク保護 ─┤ MIX
                                                                                            ↓
                                                         最終段のピーク保護(−0.2 dBFS 以上のみ)→ Out

畳み込みエンジン(dsp/ds_conv.c)

非一様分割の overlap-save 畳み込みです。

段IR の範囲分割FFT処理
HEAD0 – 4096 サンプル128 × 32256 点実 FFT毎ブロック。追加レイテンシ 0
BODY4096 – 96256 サンプル2048 × 最大 454096 点実 FFT16 ブロック(1 サイクル)に分散

BODY の 1 サイクル(128 × 16 = 2048 サンプル)の内訳は、ブロック 0/1 で左右の順 FFT、2–13 で 周波数領域の積和(45 パーティションを 12 等分)、14/15 で左右の逆 FFT。BODY を IR の 4096 サンプル目 (= 2 × 2048)から始めているので、この分散に必要な余裕がちょうど確保され、出力は時間的に正確です。

  • IR のスペクトルはすべて事前に FFT 済み(ビルド時に 1/N も畳み込み済み)。FFT のテーブルは プラグインの初期化時に 1 回だけ作り、オーディオコールバック内では初期化しない
  • 入力の周波数領域ディレイラインを 2 つのフィルタセットで共有するので、IR 切り替えは 「新しいセットを 1 サイクル並走 → 6 サイクル(約 0.28 秒)の等パワークロスフェード」で、履歴を失わない
  • ホストのブロックサイズに関係なく、内部は常に 128 サンプル単位(128 サンプルの FIFO。 その分プリディレイを短くしているので Wet の時刻は正確、ドライはレイテンシ 0)
  • 直接畳み込みとの誤差は −129 dB 以下(テスト 1)

後期残響(dsp/ds_fdn.c)

16 本のディレイライン(SIZE 1 で 17.3–96.7 ms、SIZE^0.75 倍)、16×16 Hadamard 行列(高速 Walsh–Hadamard 変換、直交 = 無損失)、各ラインに DC 利得と Nyquist 利得を RT60 から決める 1 次の吸収フィルタ、0.06–0.26 Hz の独立した微小変調(1.5–9 サンプル、3 次エルミート補間)、 出力の DC 除去、通常の 4 倍以上でだけ働く安全リミット(テストでは一度も作動していない)。

安全対策

  • NaN / Inf:Wet ブロックごとに検査し、万一出たら Wet を無音にしてメモリを消去・IR を作り直す(テストでは発生 0)
  • Denormal:処理中だけ FPCR の FZ ビット(x86 では FTZ/DAZ)を立て、終わったら元に戻す
  • オーディオスレッドでのメモリ確保・ファイル I/O・ロック・スレッド生成なし。インスタンスは静的プールから CAS で取得(scripts/check_abi.py が malloc / mutex の参照をビルドで拒否)
  • 使い回したスロットの信号履歴(約 3 MB)は 1 ブロック 128 KB ずつ消去してから使う

CPU・メモリ・レイテンシ

  • レイテンシ:0 サンプル(ドライ経路、畳み込みの HEAD とも追加遅延なし)
  • メモリ:1 インスタンス 6.6 MB(IR スペクトル 2 セット 3.1 MB、周波数領域ディレイライン 1.5 MB、 FDN 1.0 MB、IR 生成バッファ 0.73 MB、プリディレイ 0.25 MB)。静的プール 8 個分(53 MB)は 仮想領域で、使われたインスタンスの分しか実メモリを消費しない
  • CPU:下記「Move 実機での計測」を参照

ファイル構成

schwung-deep-space/
├── LICENSE                     MIT (Hiro Ikezawa)
├── README.md                   このファイル
├── THIRD_PARTY_LICENSES.md     Schwung API ヘッダ (MIT) の notice、合成 IR であることの明記
├── module.json                 id deep-space, audio_fx, chain_params 8 個
├── help.json                   本体ヘルプ (ASCII)
├── dsp/
│   ├── ds_plugin.c             audio_fx_api_v2 アダプタ、ui_hierarchy、インスタンスプール、状態保存
│   ├── ds_engine.c/.h          全体の接続、DEPTH / FREEZE / MIX、スムージング、IR の段階的ビルド
│   ├── ds_conv.c/.h            非一様分割ステレオ畳み込み、2 セットのクロスフェード
│   ├── ds_fft.c/.h             実 FFT(256 点 / 4096 点)
│   ├── ds_irgen.c/.h           6 種類の合成ステレオ IR
│   ├── ds_fdn.c/.h             16 ライン FDN
│   ├── ds_params.h             パラメータ表 (module.json と一致をビルド時に検査)
│   └── ds_dsp.h                1 次フィルタ係数、乱数、FTZ
├── third_party/schwung/        Schwung の plugin API ヘッダ (MIT, 無改変)
├── scripts/
│   ├── build.sh                native / asan / arm64 ビルド、arm64 は dist/ に tarball
│   ├── test.sh                 全テスト
│   ├── render_demo.sh          オフライン試聴用 WAV と IR の WAV
│   ├── install.py              Schwung Manager 経由のインストール
│   ├── metadata.py             module.json → DSP 埋め込み + 整合チェック
│   ├── check_abi.py            ARM64 .so の ABI 検査
│   └── check_licenses.py       コピーレフト混入・著作権表示の検査
└── tests/
    ├── test_engine.c           エンジン / プラグイン API テスト
    ├── test_grid.mjs           実機と同じページエンジンで標準グリッドのテスト
    ├── dump_contract.py        ビルドした .so の応答をグリッドテストへ渡す
    ├── render_demo.c           デモレンダラ
    ├── signals.h               ドラム / コード / ピンクノイズ等の合成テスト信号、WAV 書き出し
    └── bench_device.c          Move 実機用 CPU ベンチ (静的 aarch64 バイナリ)

チェインホストは audio_fx/<id>/<id>.so を読むので、DSP は deep-space.so です。

テスト(scripts/test.sh)

#内容結果
1分割畳み込み = 直接畳み込み(IR 3000 / 4096 / 4097 / 40000 / 96256 サンプル)、インパルス → IR そのもの誤差 −129〜−134 dB、インパルス誤差 4e-7
2ホストのブロックサイズ 1 / 37 / 64 / 300 / 1–300 ランダム vs 128出力の差 0(ビット一致)
3SPACE 6 回 + SIZE + DECAY の切り替え中の最大サンプル段差・20 ms レベル変化段差は定常時の 1.33 倍以内、レベル変化 +0.1 dB
4DECAY 1 / 2 / 5 / 10 / 20 / 30 秒 → 実測 RT60(400 Hz 以下、EDC −10〜−40 dB)1.17 / 1.10 / 1.01 / 0.99 / 0.99 / 0.98 倍
5DEPTH 0 → 100 %(6 プリセット):プリディレイ、初期/後期比、スペクトル重心、ラウドネス下表
6FREEZE を 5 分(入力は鳴らし続ける)→ 解除上昇なし、5 分で −1.7 dB 以内、DC 4.5e-6、クリックなし、解除後に減衰
7全連続パラメータの最小/最大 64 通り × フルスケール入力 × FREEZE on/offNaN/Inf 0、ピーク ≤ 1.0
8左のみ / モノ入力 → 左右に無相関な残響、ドライは元の側に残るR/L −3.9 dB、相関 0.02 / 0.05
9MIX 0 / 50 / 100 %0 % はビット一致バイパス、50 % でドライ −3 dB、100 % でドライなし
—MIX 0 で休止:計算を止める(デスクトップで 49.5 → 1.5 µs/ブロック)、40 % → 0 でもビット一致、上げ直しでクリックなし・50–100 ms 後に残響あり、FREEZE 中は休止しないOK
10インスタンス 8 個 + 9 個目は拒否、生成/破棄 200 回、再利用スロットが無音から立ち上がるOK(再利用時の準備完了まで約 0.6 秒)
—パラメータ、enum 名、状態保存(FREEZE は保存しない)、ui_hierarchyOK
—AddressSanitizer + UBSan、標準グリッド UI(実機のページエンジン)、ARM64 ABI、ライセンス検査OK

DEPTH 0 → 100 %(MIX 100 % のインパルス応答と、MIX 40 % のピンクノイズのラウドネス):

SPACE立ち上がり初期反射/後期スペクトル重心ラウドネス
CATHEDRAL40 → 246 ms−10.0 → −20.6 dB4855 → 1211 Hz+0.6 → −1.1 dB
CAVERN19 → 226 ms−5.6 → −16.5 dB3051 → 966 Hz+0.2 → −2.2 dB
TUNNEL12 → 216 ms−4.7 → −16.0 dB3945 → 1070 Hz+0.8 → −0.6 dB
HANGAR49 → 255 ms−4.8 → −13.7 dB6813 → 1154 Hz+0.9 → −0.8 dB
ABYSS74 → 286 ms−19.9 → −30.0 dB984 → 564 Hz+0.5 → −0.4 dB
VOID31 → 244 ms−18.6 → −27.3 dB6376 → 1526 Hz+0.8 → −2.0 dB

Move 実機での計測

2026-10-23、host 1.7.3、build/bench-device-arm64 を Move 上で通常優先度・taskset 0x7 で実行 (Move の CPU は Cortex-A72、1 ブロック 128 サンプル = 2902 µs)。

設定平均最悪
初期値478 µs(16.5 %)1756 µs
最大(ABYSS、SIZE 4、DECAY 30)484 µs(16.7 %)2245 µs
SPACE を 1 秒ごとに切り替え(IR 生成 + クロスフェード)569 µs(19.6 %)1747 µs
FREEZE451 µs(15.6 %)7880 µs
MIX 0(休止、0.1.1)6 µs(0.2 %)380 µs

内訳(平均):畳み込み 207 µs、FDN 177 µs。4096 点 FFT 1 回 138 µs。 最悪値は通常優先度のプロセスとして測ったもので、他プロセスによる横取りを含みます (7880 µs は明らかに横取り)。SPI スレッド内の実際の値は Schwung の CPU ページで確認してください。

既知の制約

  • IR はすべて合成です。実測(録音)IR の読み込み機能はありません
  • SPACE / SIZE / DECAY の変更は、新しい IR の生成とクロスフェードのため約 0.5 秒遅れて反映されます
  • SIZE を回している間は FDN のディレイ長が滑らかに追従するため、わずかなピッチの揺れが出ます
  • FREEZE は完全な無限持続ではありません(5 分で約 −0.6 dB、安定性優先)。FREEZE の状態は保存しません (セットを開き直したときに空の残響で凍結されているのを避けるため)
  • プリセット間の音量はピンクノイズで揃えています。キックのような低域主体の素材では、低域を削っている VOID が小さめに、強い初期反射を持つ HANGAR が大きめに聞こえます
  • IR の最大長は 2.0 秒です(それ以降は FDN が担当)