>100 Views
September 27, 26
スライド概要
■extension DC 2026 Day2 のLT資料
extension DC 2026 Day2
https://extension-dc.connpass.com/event/391185/
この資料は、音声アプリの UI で波形をリアルタイムに動かすために検証した Metal の設計にフォーカスしています。8 分・約 2,100 万サンプルのトラックを毎フレーム並べられない問題から出発し、min/max の Float 配列と LOD で持つデータ、同じ投影式に同じフレームの入力をそろえる再生線の位置合わせ、頂点を再利用して可視範囲だけ描く更新分離の 3 点を中心に解説しました。それでも残る波形のチラつきをピクセルスナップで抑えるところまで扱います。
■ GitHub
MetalWaveformKit
https://github.com/masaconm/MetalWaveformKit
■ 内容
・波形データの持ち方:一定サンプル数のバケットごとに振幅の min/max だけを残した [Float] と、ズーム(秒/px)に応じた LOD 選択
・再生線と波形位置の一致:時刻 → NDC の投影式を全要素で共有し、1 フレームの入力を 1 回だけ読む設計。1 フレーム古い入力によるずれ(drift)の実測例
・Viewport の不変条件テスト:再生位置と表示幅を変えても投影結果が中央(NDC 0)の許容誤差内に収まることを確認
・更新の分離:頂点をトラック時刻のまま不変の MTLBuffer に置き、time → NDC の変換は頂点シェーダで行う。更新をトラック読込時/LOD 初回使用時/毎フレームの 3 つに分ける
・毎フレーム再構築とキャッシュ再利用の CPU 時間比較(draw(in:) の中央値)
・ピクセルスナップ:カメラ開始位置を 1 drawable pixel 単位で丸めて移動時の明滅を抑える仕組みと、24 倍拡大での ON/OFF 比較
・まとめ:データ/投影/更新の 3 つの設計ポイント
■ 本資料で解説をした技術:
・Swift(解析結果を不変の Sendable な値型として共有)
・Metal(MTLBuffer の再利用、Metal Shading Language による頂点シェーダでの投影)
・Swift Testing(Viewport の不変条件テスト)
・LOD(min/max エンベロープの段階的な結合)
・ピクセルスナップ(drawable pixel 単位の丸め)
■ 備考:デモアプリケーション全体で使用しているが解説を省略した技術
・GPU 側の最適化と、ラスタライズ結果の検証
・音声出力との同期精度と、出力レイテンシの補正
・AVAudioSourceNode のレンダー内部と、ロックフリー化
・Swift Concurrency の設計(actor 分離・Sendable 準拠の詳細)
・SwiftUI Canvas / Core Graphics との性能比較
・4x MSAA、CADisplayLink による表示更新、SwiftUI ラッパー(MetalWaveformKit には含まれるが本資料では扱わない)
■ 参考ドキュメント
■ 参考ドキュメント
AVAudioEngine | Apple Developer Documentation
https://developer.apple.com/documentation/avfaudio/avaudioengine
AVAudioSourceNode | Apple Developer Documentation
https://developer.apple.com/documentation/avfaudio/avaudiosourcenode
AVAudioSession.outputLatency | Apple Developer Documentation
https://developer.apple.com/documentation/avfaudio/avaudiosession/outputlatency
Metal | Apple Developer Documentation
https://developer.apple.com/documentation/metal
MTKView | Apple Developer Documentation
https://developer.apple.com/documentation/metalkit/mtkview
Metal Shading Language Specification
https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
Swift Testing | Apple Developer Documentation
https://developer.apple.com/documentation/testing
Sendable | Apple Developer Documentation
https://developer.apple.com/documentation/swift/sendable
Concurrency | The Swift Programming Language
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/
Canvas | Apple Developer Documentation
https://developer.apple.com/documentation/swiftui/canvas
Optimizing iPhone and iPad apps to support ProMotion displays | Apple Developer Documentation
https://developer.apple.com/documentation/quartzcore/optimizing-iphone-and-ipad-apps-to-support-promotion-displays
The Scientist and Engineer's Guide to Digital Signal Processing (Steven W. Smith)
https://www.dspguide.com/
Metalでやってみた リアルタイム音響アプリ波形アニメーション設計 masacom @masacom
自己紹介 職業:フロントエンドエンジニア 個人開発:Swiftで音響アプリ開発をしています mascom @masacom 本日のサンプル:MetalWaveformKit URL: https://github.com/masaconm/MetalWaveformKit.git 技術検証サンプルの本体となる10 Band EQアプ リは現在 TestFlight 中です。ご興味がありました らお声がけください!
SAMPLE:MetalWaveformKit 波形描画ライブラリ / サンプルアプリ / テスト・日英ガイドのご案内 音響アプリケーション開発で検証した波形描画の設計をSwiftパッケージにまとめました サンプルアプリは合成データで波形を動かします(音声再生なし) 導入・操作方法・検証条件は、リポジトリの日英ガイドを参照してください URL: https://github.com/masaconm/MetalWaveformKit.git
話すこと UI 波形から音をリアルタイムに操作するために検証したこと time-domain データを Float 配列で扱う更新単位 時刻から画面位置への投影と、フレーム入力の共有 1 フレームごとの頂点全再構築を避ける更新分離 それでも残る波形の揺れへの対処(ピクセルスナップ) リアルタイム制御をする波形アプリ設計の 3 つのポイント
話さないこと GPU 側の最適化と、ラスタライズ結果の検証 音声出力との同期精度と、出力レイテンシの補正 AVAudioSourceNode のレンダー内部と、ロックフリー化 Swift Concurrency の設計(actor 分離・Sendable 準拠の詳細) SwiftUI Canvas / Core Graphics との性能比較
実装したい機能と課題
UIで波形のリアルタイム操作を実装する際の課題 音声アプリ開発時に起きた課題 1.再生位置のズレ 再生線と再生位置の不一致・ブレ 2.波形線の揺れ・乱れ 音声再生時に波形がチラついて見える 3.操作時の遅延・もたつき 波形スクラブが音声に追従しない 等 RenderingDemoの波形・操作部(合成データ・音声なし) Point データ・投影・更新周期の 3 点で整理が必要
データの持ち方で悩む
データの持ち方 😩 悩んだこと 例:8 分(約 2,100 万サンプル) にもなるトラックを 丸ごとフレーム画面へ並べたら読み込みすら困難 検証したこと min/max の Float 配列と LOD で、必要な細かさだけ持たせる
波形データは min/max エンベロープの [Float] 一定サンプル数のバケットごとに、振幅の最小値と最大値だけを残す 1 バケットを拡大 例:8 サンプル / 実装は 32 サンプル/バケット max 0.9 min 0.6 0.2 0.9 0.6 0.3 0.2 0.4 0.1 [ min₀, max₀, min₁, max₁, … ] Point 0.4 2 つの Float 時刻 → index は O(1)。解析結果は不変の Sendable な値型として共有する
ズームに応じて LOD(解像度段階)を選ぶ 同じ停止位置で異なる倍率を表示する場合 RenderingDemo:32.000秒で停止(iPad・合成データ・音声なし) 1x LOD 2 4x 16.00 秒 を表示 LOD 0 64x 4.00 秒 を表示 LOD 0 0.25 秒 を表示 拡大すると、より細かい LOD へ Point 隣接バケットの min/max を結合し、秒/px に応じて LOD を選ぶ ※UI波形はPCMのmin/maxを集約した表示であり、アナログ信号の連続波形 そのものではありません。
再生線と波形位置の不具合
再生線と波形位置を合わせる 😩 悩んだこと 同じ式で計算しても再生線のブレやずれが発生 検証したこと 同じ式に、同じフレームの入力をそろえる
同じ投影式でも、1 フレーム古い再生位置ならずれる 発表時のデモ取得例:ズーム4 /表示時刻 2:19.651 Synced 0.0 px 1-frame stale 6.4 px ※HUDは小数1桁で表示。0.0 pxには、厳密な0と表示上の丸めによる0が含まれます。 Point drift は 2 つの再生時刻を同じ viewport で投影した x の差(px) 出力レイテンシの測定ではない
同じ投影式 + 同じフレームの入力 描画の入力を1回読み、波形・キュー・拍グリッドで viewport を共有する 公開OSSの再生線は画面中央に固定する 要素ごとに別のタイミングで読む場合 波形:いまの値 再生線:前の値 同じ投影式 表示位置がずれる 1回読んで全要素で共有する場合 今フレームの入力 playhead / zoom 波形・キュー 拍グリッド 同じ viewport 同じ time → x xNDC = ((t visibleStartSec) / viewportSec) 21
不変条件をViewportでテストする 再生位置と表示時間幅を変え、同じ入力からカメラを作り、 投影結果が中央(NDC 0)の許容誤差内に収まることを確認 ※0は浮動小数点でも正確に表せます。ただし、演算結果には丸め誤差が生じ得ます。 ※本CPUテストはスナップ適用前の |xNDC| < 10⁻⁵(NDC単位)を確認します。
readFrame はオーディオレンダー側の読み出し位置 発表時の音声付きデモの実装例(音声再生はOSSの対象外) 描画が参照する再生位置 SourceNode PCM を生成 readFrame 次に読む位置 毎フレーム pull 位置を投影 currentTimeSec = readFrame / sampleRate 音が実際に出るまで(出力レイテンシ) 生成済み PCM エンジン・出力経路 注) outputLatency と表示遅延の補正は、本トークでは対象外とする 耳へ届く音
フレームに読み込む頂点の考え方
毎フレーム読み込む頂点数が多すぎる問題 😩 悩んだこと 全頂点の毎フレーム再構築でCPU負荷が増え、 操作に使える時間が減る 検証したこと 頂点をトラック時刻で保持し、更新を 3 つのタイミングに分ける
全頂点の毎フレーム再構築は CPU 時間を消費する 保存済み CSV例:2〜26 秒・各 47 点 / draw(in:) の CPU 経過時間の中央値 毎フレーム再構築 キャッシュ再利用 5.336 ms 0.060 ms HUD の取得例(上の中央値とは別) 発表時の検証デモでの測定例です。OSS配布版での再計測値ではありません。 Point fps 中央値は両方 60.0。フレーム予算内の CPU 余裕が増えた
頂点座標はトラック時刻(秒)のまま GPU に置く 不変の MTLBuffer を再利用し、time → NDC の変換は頂点シェーダで行う Xcode / Shaders.metal (timeSec, amplitude) time → x 画面上の波形 保持した頂点 viewport で投影 可視範囲だけ描画
更新を 3 つのタイミングに分ける 毎フレームの全頂点生成を、LOD ごとの初回構築(キャッシュ)へ移す Xcode / WaveformRenderer.swift トラック読込時 各 LOD の初回使用時 PCM 解析 → min/max → LOD MTLBuffer を構築・保存 Point 毎フレーム 入力・uniform・可視範囲 nil のときだけ構築。同じ LOD に戻ると再利用する。
それでも残った波形の揺れ・チラつき
波形の明滅・ちらつきの原因を調査 😩 悩んだこと 位置も負荷も調整済みだが波形が明滅している 波形線内部が動いてる?ちらつきが止まらない! 検証したこと カメラの開始位置を 1 drawable pixel 刻みへ丸める
ピクセルスナップで移動時の明滅を抑える カメラの開始位置を 1 drawable pixel 単位で丸める スナップなし:ピクセル被覆率が毎フレーム変わる 小数部分が変わる 画素の被覆率が変動し、揺れて見える frame 0:2.3 px frame 1:2.7 px 1px 単位:被覆率を保ったまま平行移動 整数ピクセルの平行移動 固定ズームで位相が一定となる frame 0:2 px frame 1:3 px ※上記は、1 セル = 1 drawable pixel。幅 1 px の要素を例にした概念図です ※固定ズームでの明滅を抑える。代わりに最大 0.5 px の丸め誤差と段階的なスクロール
ピクセルスナップを確認 入力を 0 px 0.25 px に1秒ごとに切り替え、黄色い枠内の縁を動画で比較 Snap ON は同じ画素を保ち、OFF では縁の明るさが変わり波形線の乱れとなる RenderingDemoの実描画2 状態を交互表示。比較画面 で24倍に拡大した画像を使 用 位置の丸め誤差は最大0.5描 画ピクセルで、移動は段階的 になります(固定ズームで比 較)
まとめ
設計のポイントは3つ 1. データ min/max の不変配列 + LOD 2. 投 影 同じ式 + 同じフレームの入力 3. 更 新 頂点を再利用し、可視範囲だけ描く Point 波形をMetalで動かすとき、変わるものと変わらないものを分ける設計を 守ることで、UI上での波形・音声のリアルタイム操作UXを向上できる
ご清聴ありがとうございました!
参考ドキュメント AVAudioEngine https://developer.apple.com/documentation/avfaudio/ avaudioengine AVAudioSourceNode https://developer.apple.com/documentation/avfaudio/ avaudiosourcenode AVAudioSession.outputLatency https://developer.apple.com/documentation/avfaudio/ avaudiosession/outputlatency Metal https://developer.apple.com/documentation/metal MTKView https://developer.apple.com/documentation/metalkit/mtkview Metal Shading Language Specification https://developer.apple.com/metal/Metal-Shading-LanguageSpecification.pdf Swift Testing https://developer.apple.com/documentation/testing Sendable https://developer.apple.com/documentation/swift/sendable Swift Concurrency(The Swift Programming Language) https://docs.swift.org/swift-book/documentation/the-swiftprogramming-language/concurrency/ SwiftUI Canvas https://developer.apple.com/documentation/swiftui/canvas ProMotion displays https://developer.apple.com/documentation/quartzcore/optimizingiphone-and-ipad-apps-to-support-promotion-displays The Scientist and Engineer's Guide to DSP(S. W. Smith) https://www.dspguide.com/