Slide 1

Slide 1 text

再履修: 2023年までの リアルタイムオーディオ処理 atsushieno

Slide 2

Slide 2 text

このLTの趣旨 これまでにオーディオ開発者向けに公開されたセッション動画や資料をもとに、リアルタ イムオーディオ処理にまつわる(主にC++の)知見をまとめていきます いろんなテクニカルタームを出していきます 特にオチはないので時間のある限り話していきます

Slide 3

Slide 3 text

参考資料(テキスト、動画) Real-Time audio processing 101: time waits for nothing (Ross Bencina) 🔗 ADC19 - Real-Time 101 Pt. I / Pt. II ADC21 - C++ Standard Library for Real-time Audio 🔗 "Allocators: the Good Parts" 🔗 "practical memory pool based allocators for modern C++" ADC22 - Thread synchronisation in real-time audio processing with RCU (Read-Copy-Update) The Ardour DAW – Latency Compensation and Anywhere-to-Anywhere Signal Routing Systems ...

Slide 4

Slide 4 text

リアルタイムオーディオ処理 一定時間に必ず処理を終えないと音飛び・ノイズの原因になる 処理速度は重要だが、「一定時間に必ず処理を終える」ことが最優先 ※WCET (worse case execution time) が数msec.以下になること オーディオ処理はリアルタイムスレッドで行う オーディオ処理以外との協調動作は適切に実装する必要がある(GUI, I/Oなど)

Slide 5

Slide 5 text

リアルタイム処理の一般原則 システムコールは基本的にNG - ロックNG - メモリ確保NG 計算量 O(1) より大きいものNG 例外はNG C++コルーチンはNG

Slide 6

Slide 6 text

メモリ確保 動的メモリ確保 / 解放を避ける(システムコール) - 解放も(開放時にシステムが複雑なコンパクションを行う可能性) std::shared_ptrをatomicに操作することはできない std::vectorのコピー操作なども動的メモリ確保/解放がありうる コピーコンストラクタやコピー代入演算子も潰しておくのが安全(` = deleted`)

Slide 7

Slide 7 text

メモリ確保 静的メモリ確保で何とかする - リングバッファ - choc::SmallVector: 確保済みメモリでstd::vectorライクなAPI - choc::sorting::stable_sort : 動的メモリ確保しないstd::stable_sortライクなAPI - static_vector?

Slide 8

Slide 8 text

動的メモリ確保はもはや禁忌ではない? std::pmr (polymorphic memory resource) でリアルタイム動的メモリ確保 - libc++で未実装 / cradleapps/realtime_memory (プライベート実装) - 事前アロケーション、 single threaded, O(1) - std::pmr::monotonic_buffer_resource, std::pmr::unsynchronised_pool_resrouce, std::pmr_polymorphic_allocator - copy constructorやmoveなどでallocatorはコピーされない(!) - std::mapではなくstd::pmr::mapが用意されている ※tcmalloc、rpmalocなどモダンな汎用アロケーターもnon RT-safe ※原理原則は変わっていない  (これらはシステムレベルの確保/解放を行わない)

Slide 9

Slide 9 text

オーディオスレッドと排他制御 ADC19 Real Time 101のフローチャート(25分目くらい) 「複数のスレッドではどうデータ共有するのが適切か?」→ 条件による

Slide 10

Slide 10 text

オーディオスレッドと排他制御 全部説明はできないので簡潔に - データ共有はない? → FIFO - RTスレッドはない? → std::mutex - 小さいデータ? → std::atomic - リソース確保失敗してもよい? → try_lock - RTスレッドがデータを更新しない? → CAS - non-RTスレッドがデータを更新しない? → ダブルバッファリング - どっちも更新しうる → CAS + FIFO でがんばる… ※CAS: (atomic) compare and set (or swap)

Slide 11

Slide 11 text

オーディオスレッドと排他制御 補足: 「小さいデータ」判定: std::atomic::is_always_lock_free volatileはメモリモデルの規定がないので std::atomicを使うべき 「リソース確保の失敗してもよい」場合 - ウェーブテーブルの更新など(空白で再生) std::try_to_lockはOKだがstd::mutexはリリース時にブロックの可能性があるので NG (多分そんな実装は多くないけど標準としては wait-freeではない) std::mutexの代わりにstd::atomic_flagを使った単純なspin lockを使えばOK CAS loop : non-atomicな構造体をオーディオループ外から更新する場合など ABA問題など各種の罠を回避できる farbot::NonRealtimeMutableが便利

Slide 12

Slide 12 text

オーディオスレッドと排他制御 double buffering (RTスレッドが更新するスペクトラムを non-RTスレッドが読む場合 etc.) farbot::RealtimeMutableが便利 RTスレッドもnon-RTスレッドもデータを操作しうる場合 リングバッファを活用したfarbot FIFOが有用。ただオプションが多い : - single consumerかmulti consumerか? - single producerかmulti producerか? - ロック確保失敗時nullを返してもいいか? - etc... - 複数スレッドが変更するのはほぼ無理なので 変更は1つのスレッドに任せるべき

Slide 13

Slide 13 text

RCU (read-copy-update) RTスレッドとnon-RTスレッドでのデータ共有はLinuxなどでRCUとして一般的 Deferred reclamation (遅延delete) crill-dev/crill on GitHub - seqlock_object : load(), try_load() and store() - readerとリソースにgenerationを付けるみたいな実装

Slide 14

Slide 14 text

非同期呼び出し タスクを投げっぱなしにするのは賢いやり方 RT safeに投げる仕組み(non-allocating, non-blocking)を使う JUCE::callAsync(), farbot::AsyncCallerなど lambdaそのものを渡すのはOK lambdaを使うときにはアロケーションが発生しうる

Slide 15

Slide 15 text

>O(1): 計算時間が未知の処理 (ならし計算量, 償却計算量)でのO(1)もNG STL: std::arrayだけはRT-safe、他はallocatorなどに依存 - 要素型とiteratorの型がRT-safeであることは必須

Slide 16

Slide 16 text

例外 throw - not RT-safe tryブロックに入って例外を投げないうちはRT-safe? -> YESっぽい - x86以外はRT-safe / x86: 実行時にunwindを動的に生成 関連: std::variantはRT-safeだがboost::variantはnot RT-safe - 例外発生時に例外オブジェクトを一時退避確保する挙動 MSのZero-overhead deterministic exceptionsのC++ draft

Slide 17

Slide 17 text

その他 CPUキャッシュを活用できるようにコンテキストスイッチをなるべく回避する 型消去 : non RT-safe (std::any, std::function, ...) コルーチン: フレームメモリ確保が発生 rand() : 実装が「生成した数が重複しないようにロックする」可能性がある