長時間録音でCaptureを解析から分離するbounded-memory設計
AVAudioEngineのcallbackからwriterまでをhard-boundedにし、CaptureLifecycleとpartial chunkで長時間録音を壊しにくくする実装。
録音開始のcritical pathから解析を外す
録音開始時に必要なのはpermission、audio session、input format、出力先、capture開始、最初のbuffer永続化です。VAD、diarization、speaker embedding、ASR、denoise、cloud requestは開始条件に含めません。
解析モデルのwarm-upやnetwork待ちをrecord tapの前に置くと、その遅延がそのまま欠損になります。後処理は再実行できますが、録音されなかった音声は復元できないため、CaptureとAnalysisを異なるfailure domainとして扱います。
CaptureLifecycleをbooleanの組み合わせにしない
CaptureLifecycleはidle、starting、capturing、writerFailed、stoppingFinalize、stoppingAbandonの6状態です。acceptsCallbacksはcapturingだけtrueで、stop時には正常終了ならstoppingFinalize、writer failure後ならstoppingAbandonへ遷移します。
特にstop開始後、queue済みwriteが失敗するケースを分けています。stoppingFinalize中にrecordWriterFailureが発生するとstoppingAbandonへ落とし、壊れたpartial chunkをfinalized fileとしてpublishしません。
writer queueを無制限に積まない
CaptureBackpressureGateはpending件数へhard capacityを持つnon-blocking gateです。実アプリのInstantCaptureRecorderではcapacity 8を設定しています。capture callbackはslotを取れればwriterへ渡し、満杯ならwaitせずrejectedを増やしてfalseを返します。
重要なのは、writerが遅いときにcallback threadをblockすることでも、無制限queueを作ることでもありません。capacity、pending、highWaterMark、rejectedをsnapshotとして観測できるため、処理遅延を「メモリが増えた」という事後現象ではなくbackpressure metricとして扱えます。
active partialとfinalized chunkを区別する
長時間session全体を一つの巨大fileへ依存させず、短いCaptureChunkへstream writeします。active chunkはpartialとして扱い、durably closeできたものだけをfinalizedとしてpublishします。
cleanupはSpeechClipへの必要音声とtime mappingがdurably commitされた後だけ許可します。VADやderived processingが失敗した場合にsource chunkを残せるため、解析失敗がcapture lossへ連鎖しません。
時間軸はwriter完了時刻から復元しない
各chunkにはabsolute start/end、session-relative start、captured duration、timing provenanceを持たせ、capture callback由来の時刻をauthorityにします。writer completion timeやdecoded durationの累積から後付けで実時間を作りません。
interruptionでchunk間にgapがあっても、そのgapをSpeechClip側へ保持できます。長い無音を保存対象から除外しても、「現実の何時何分に発話したか」を失わないための設計です。