Upgrade to Pro — share decks privately, control downloads, hide ads and more …

リアルタイム音声処理を題材に、 Kotlin Flowのバッファリングと 実行モデルを改めて深...

Avatar for tkc tkc
September 04, 2026

リアルタイム音声処理を題材に、 Kotlin Flowのバッファリングと 実行モデルを改めて深掘りする

Avatar for tkc

tkc

September 04, 2026

Other Decks in Programming

Transcript

  1. ⾳声をデジタルで扱う [0, 16123, 7640, -6873, -18385, …] = ただの ShortArray

    ① 波形(アナログ) ② 等間隔で測る=サンプリング ③ 数値の列=PCM 7
  2. このアプリが扱う⾳声 48000Hz / ステレオ2ch / 16bit PCM 1 / 48000

    秒 縦軸:16bit 65,536段階 … 途中省略 1フレーム = 20ms 8
  3. Androidで⾳声を再⽣するには? val format = AudioFormat.Builder() .setSampleRate(48000) .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .build() val

    audioTrack = AudioTrack.Builder().setAudioFormat(format).build() audioTrack.write(pcm, size, AudioTrack.WRITE_BLOCKING) OSに音声データを渡す必要がある。その受け口が AudioTrack 9
  4. Androidで⾳声を再⽣するには? val format = AudioFormat.Builder() .setSampleRate(48000) .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .build() val

    audioTrack = AudioTrack.Builder().setAudioFormat(format).build() audioTrack.write(pcm, size, AudioTrack.WRITE_BLOCKING) サンプルレート・チャンネル数・ビット深度は、作るときに決める 10
  5. Androidで⾳声を再⽣するには? val format = AudioFormat.Builder() .setSampleRate(48000) .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .build() val

    audioTrack = AudioTrack.Builder().setAudioFormat(format).build() audioTrack.write(pcm, size, AudioTrack.WRITE_BLOCKING) サンプルレート・チャンネル数・ビット深度は、作るときに決める 11
  6. ⼿続き的な設計:2つのwhileループ • デコード担当と再⽣担当の2クラスに分かれ、間はChannelで繋ぐ構成 • 各クラスの中⾝は1つのwhileループであり、処理順序がコードに固定化 • 機能追加のたびに、ループ処理の本体コードを直接編集する必要が発⽣ LiveBgmPlayer の while

    (true) ポーズ判定(300msポーリング) AudioDecoder の while (true) Channel capacity=10 MediaCodecの⼿動出し⼊れ アップサンプル → ⾳量 フレーム整形(frameSize単位) ステレオ → 設定チャンネル数 再⽣位置の更新 ローカル再⽣ と 配信送信 リングバッファへ詰め替え 処理の順番はループに固定で、部品として差し替えられなかった 18
  7. 宣⾔的な設計:Flowのパイプライン Consumer 使う Producer 作る Intermediary 加⼯する Flow Flow ローカル再⽣

    フレーム整形 ⾳源デコード リモート送信 アップサンプル マイク⾳声取得 再⽣位置保持 チャンネル変換 ポーズ制御 19
  8. 部品が差し替え可能になった Consumer 使う Producer 作る Intermediary 加⼯する Flow ローカル再⽣ フレーム整形

    リモート送信 アップサンプル ⾳源デコード 再⽣位置保持 チャンネル変換 ポーズ制御 24
  9. 部品が差し替え可能になった Consumer 使う Producer 作る Intermediary 加⼯する Flow ローカル再⽣ フレーム整形

    リモート送信 アップサンプル マイク⾳声取得 再⽣位置保持 チャンネル変換 ポーズ制御 25
  10. 部品が差し替え可能になった Consumer 使う Producer 作る Intermediary 加⼯する Flow ローカル再⽣ フレーム整形

    リモート送信 アップサンプル マイク⾳声取得 チャンネル変換 再⽣位置保持 ポーズ制御 26
  11. Producer / Intermediary / Consumer interface IAudioStreamProducer<T> { fun produce():

    Flow<T> } interface IAudioStreamIntermediary<TU, TD> { fun transform(upstream: Flow<TU>): Flow<TD> } interface IAudioStreamConsumer<T> { suspend fun consume(data: T) } T は⾳声データ。中⾝はPCMのバイト列を持つ ByteBuffer 27
  12. Producerの実装例:⾳声ファイルをデコードする class MediaDecodeAudioStreamProducer(...) : IAudioStreamProducer<...> { override fun produce(): Flow<...>

    = flow { val decoder = MediaCodec.createDecoderByType(mimeType)... while (!isDoneRead || !isDoneWrite) { yield() decoder.enqueueSampleIfPossible(...) // 読む when (val r = decoder.dequeueSampleIfPossible()) { is DequeuedSample -> emit(...) // 流す else -> {} } } } } 29
  13. Intermediary override fun transform(upstream: Flow<StreamingAudioData.MediaCodecExtractedResult>): Flow<StreamingAudioData.MediaCodecExtractedResult> { return upstream.transform {

    result -> var offset = 0 while (ary.size - offset >= frameSizeInBytes) { val frameBuffer = audioRingBufferHolder.getNextBuffer().apply { clear() put(ary, offset, frameSizeInBytes) flip() } emit( StreamingAudioData.MediaCodecExtractedResult(...), ) offset += frameSizeInBytes } 31
  14. Intermediaryの実装例:フレーム整形 override fun transform(upstream: Flow<StreamingAudioData.MediaCodecExtractedResult>): Flow<StreamingAudioData.MediaCodecExtractedResult> { return upstream.transform {

    result -> var offset = 0 while (ary.size - offset >= frameSizeInBytes) { val frameBuffer = audioRingBufferHolder.getNextBuffer().apply { clear() put(ary, offset, frameSizeInBytes) flip() } emit( StreamingAudioData.MediaCodecExtractedResult(...), ) offset += frameSizeInBytes } 33
  15. Intermediaryの実装例:フレーム整形 override fun transform(upstream: Flow<StreamingAudioData.MediaCodecExtractedResult>): Flow<StreamingAudioData.MediaCodecExtractedResult> { return upstream.transform {

    result -> var offset = 0 while (ary.size - offset >= frameSizeInBytes) { val frameBuffer = audioRingBufferHolder.getNextBuffer().apply { clear() put(ary, offset, frameSizeInBytes) flip() } emit( StreamingAudioData.MediaCodecExtractedResult(...), ) offset += frameSizeInBytes } 34
  16. Consumer:端末で⾳声を再⽣する override suspend fun consume(data:DualChannelAudioBuffer) { val stereoBuffer = data.stereoBuffer

    // 再生用 audioTrack.write( stereoBuffer, stereoBuffer.remaining(), AudioTrack.WRITE_BLOCKING) 37
  17. Consumer:端末で⾳声を再⽣する override suspend fun consume(data:DualChannelAudioBuffer) { val stereoBuffer = data.stereoBuffer

    // 再生用 audioTrack.write( stereoBuffer, stereoBuffer.remaining(), AudioTrack.WRITE_BLOCKING) 38
  18. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 39
  19. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 40
  20. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 41
  21. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 42
  22. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 43
  23. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 44
  24. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 45
  25. パイプラインの組み⽴て val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ • • intermediateWith / consumeWith は数珠つなぎの形で記述できるようにするための自作の 拡張関数。 それぞれは単純な transformとconsumeのラッパー 46
  26. パイプラインを調査する val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary)

    .shareIn(this) // ← 1本の上流を共有する launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } // 配信へ launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 再生へ 51
  27. Flowは cold と hot に分かれる 比較項目 Cold Flow Hot Flow

    生成タイミング collect() 呼び出し時に毎回生成開始 コレクトの有無に関わらず生 成・動作 データ保持 状態を持たない(過去値は保 持しない) 最新値や履歴( Replay)を保 持可能 コレクターの関係 1対1(コレクターごとに独立し て実行) 1対多(複数のコレクターでス トリーム共有) 代表的な API flow { ... }, flowOf(), map StateFlow, SharedFlow 停止タイミング コレクトをキャンセルすると処 理も停止 コレクト停止後もバックグラウ ンドで動作し得る 57
  28. Flowは cold と hot に分かれる ⾳声処理 Cold ⾳声処理 ⾳声処理 ローカル再⽣

    リモート送信 New Consumer ローカル再⽣ Hot ⾳声処理 リモート送信 New Consumer 59
  29. shareIn が返す SharedFlow とは MutableSharedFlow<T>( replay: Int = 0, extraBufferCapacity:

    Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ) バッファの⼤きさと、あふれたときの挙動を、作るときに指定できる 65
  30. バッファなし:extraBufferCapacity = 0 val sharedFlow = MutableSharedFlow<Int>(extraBufferCapacity = 0) launch

    { sharedFlow.collect { value -> println(" [Collector] 受信: $value") delay(1000) } } // 接続待ち delay(1000) for (i in 1..3) { println("[Emit] 送信開始: $i") sharedFlow.emit(i) // コレクターの処理完了までサスペンドして待つ println("[Emit] 送信完了: $i") } 66
  31. バッファなし:Producer が直接 Consumer に 渡す Producer (送信) Consumer (受信) A,B,C

    の値を連続送信 処理が遅い (1秒) flow.emit(i) delay(1000) 67
  32. バッファなし - Step 1 extraBufferCapacity = 0 ▼ 順序 →

    Step 1 Producer A バッファ — Consumer A を受信 Step 2 Step 3 Step 4 Step 1 Producer が A を送信。Consumer が直接 A を受け取って処理を開始します。 68
  33. バッファなし - Step 2 extraBufferCapacity = 0 ▼ 順序 →

    Step 1 Producer A Step 2 Step 3 Step 4 B SUSPEND バッファ — — Consumer A を受信 A を処理中… Step 2 — サスペンド発⽣ Consumer が A を処理中のため B を受け取れません。バッファが無いので、Producer は即座にサスペンドします。 69
  34. バッファなし - Step 3 extraBufferCapacity = 0 ▼ 順序 →

    Step 1 Producer A Step 2 B Step 3 Step 4 B 送信完了 SUSPEND バッファ — — — Consumer A を受信 A を処理中… B を受信 Step 3 — サスペンド解消 Consumer が A を終えて B を受け取ります。待たされていた Producer の B 送信が完了します。 70
  35. バッファなし - Step 4 extraBufferCapacity = 0 ▼ 順序 →

    Step 1 Producer A Step 2 B Step 3 B 送信完了 SUSPEND Step 4 C SUSPEND バッファ — — — — Consumer A を受信 A を処理中… B を受信 B を処理中… Step 4 — 再びサスペンド Producer が C を送ろうとしますが、Consumer が B を処理中のため再びサスペンド。バッファが無いと毎回⾜⽌めされま す。 71
  36. バッファあり:extraBufferCapacity = 2 // バッファ枠を 2 つ確保 val flow =

    MutableSharedFlow<Int>(extraBufferCapacity = 2) // 処理の遅いコレクター( 1秒かかる) launch { flow.collect { value -> println(" [Collector] 受信: $value") delay(1000) } } delay(100) // 接続待ち for (i in 1..4) { println("[Emit] 送信開始: $i") flow.emit(i) // バッファに空きがあれば待たずに即完了する println("[Emit] 送信完了: $i") } 72
  37. バッファあり - Step 1 extraBufferCapacity = 2 ▼ 順序 →

    Step 1 Producer A バッファ — Consumer A を受信 Step 2 Step 3 Step 4 Step 5 Step 6 Step 1 Producer が A を送信。Consumer が直接 A を受け取って処理を開始します(バッファは空のまま)。 74
  38. バッファあり - Step 2 extraBufferCapacity = 2 ▼ 順序 →

    Step 1 Step 2 Producer A B バッファ — B Consumer A を受信 A を処理中… Step 3 Step 4 Step 5 Step 6 Step 2 Consumer が A を処理中に Producer が B を送信。バッファに [ B ] がたまり、Producer は⽌まらず先へ進めます。 75
  39. バッファあり - Step 3 extraBufferCapacity = 2 ▼ 順序 →

    Step 1 Step 2 Step 3 Producer A B C バッファ — B B, C (満杯) Consumer A を受信 A を処理中… A を処理中… Step 4 Step 5 Step 6 Step 3 Producer が C を送信。バッファに [ B, C ] が⼊り、容量の上限(2個)に達して満杯になります。 76
  40. バッファあり - Step 4 extraBufferCapacity = 2 ▼ 順序 →

    Step 1 Step 2 Step 3 Producer A B C Step 4 Step 5 Step 6 D SUSPEND バッファ — B B, C (満杯) B, C (満杯) Consumer A を受信 A を処理中… A を処理中… A を処理中… Step 4 — サスペンド発⽣ Producer が D を送ろうとしますが、バッファが満杯のため送信が中断(サスペンド)されます。Consumer はまだ A を処理 中です。 77
  41. バッファあり - Step 5 extraBufferCapacity = 2 ▼ 順序 →

    Step 1 Step 2 Step 3 Producer A B C Step 4 D Step 5 Step 6 D 送信完了 SUSPEND バッファ — B B, C (満杯) B, C (満杯) C, D Consumer A を受信 A を処理中… A を処理中… A を処理中… B を処理中… Step 5 — サスペンド解消 Consumer が A を終えて B を受け取り、バッファから B が消えます。空いた枠に D が⼊り、サスペンドが解けます。 78
  42. バッファあり - Step 6 extraBufferCapacity = 2 ▼ 順序 →

    Step 1 Step 2 Step 3 Producer A B C Step 4 D Step 5 D 送信完了 SUSPEND Step 6 E SUSPEND バッファ — B B, C (満杯) B, C (満杯) C, D C, D (満杯) Consumer A を受信 A を処理中… A を処理中… A を処理中… B を処理中… B を処理中… Step 6 — 再びサスペンド Producer が E を送ろうとしますが、バッファは [ C, D ] で再び満杯。ふたたびサスペンドします。 79
  43. バッファの⼤きさ = 読まれていない値を溜めておける数 Producer Consumer バッファ 0 Consumerが処理を終えるまでProducerは次の値をEmitできない Producer Consumer

    バッファ 2 バッファが満杯になるまでProducerは次の値をEmitできる。 バッファが満杯になればProducerはバッファが空くまで次の値を Emitできない。 80
  44. バッファを指定する引数はない public fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay:

    Int = 0 ): SharedFlow<T> shareIn で決められるのは、購読のタイミングと再送の件数だけ 82
  45. デフォルトで64件 For BufferOverflow.SUSPEND (the default buffer overflow strategy), the default

    capacity is 64, https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channe l/-factory/-b-u-f-f-e-r-e-d.html 86
  46. 他にもバッファがあった val sharedFlow = producer.produce() .intermediateWith(FrameSizeChunkAudioStreamIntermediary(30, frameSize)) .intermediateWith(UpSampleIntermediary(...)) ↑リングバッファ 30スロット

    .intermediateWith(playbackPositionTrackingIntermediary) .intermediateWith(stereoToConfiguredChannelIntermediary) .shareIn(this) ← SharedFlow のバッファ 64件(デフォルト) launch { sharedFlow.consumeWith(liveStreamingSendConsumer) } launch { sharedFlow.consumeWith(audioTrackWriteConsumer) } // 配信へ // AudioTrackへ 88
  47. SharedFlow に並ぶ参照(最⼤ 64 個) 参照1 が指すのは slot 1 書き込み位置 1

    1 … … 64 slot 1 Step 1 — スロット1にフレーム1が書かれる リングバッファ ⼀周 = 30スロット Producer が slot 1 にフレーム1を書き、その参照を SharedFlow へ渡す。 90
  48. SharedFlow に並ぶ参照(最⼤ 64 個) 参照1 が指すのは slot 1 1 1

    2 3 … 8 … 64 slot 1 Step 2 - バッファがあるのでEmitは続く バッファが64あるので Producer はEmitを続ける。 書き込み位置は円をぐるりと進んでいく。 リングバッファ ⼀周 = 30スロット 書き込み位置 91
  49. SharedFlow に並ぶ参照(最⼤ 64 個) 参照1 が指すのは slot 1 書き込み位置 1

    1 2 3 … 30 … 64 slot 1 Step 3 — リングバッファの30スロットが埋まる リングバッファ ⼀周 = 30スロット Producerが30回Emitしてリングバッファのスロット がすべて埋まった。 しかし、SharedFlow側のバッファはまだ空きがある ため、Emitが続く。 92
  50. SharedFlow に並ぶ参照(最⼤ 64 個) 参照1 が指すのは slot 1 書き込み位置 31

    1 2 3 … 31 … 64 slot 1 Step 4 — 上書きされる リングバッファ ⼀周 = 30スロット まだ渡していないフレーム1の上に、フレーム31が書 き込まれる。 93
  51. SharedFlow に並ぶ参照(最⼤ 64 個) 参照1 が指すのは slot 1 書き込み位置 31

    1 2 3 … 31 … 64 slot 1 Step 5 — Consumerは上書き後に読んでいた リングバッファ ⼀周 = 30スロット Consumer が読むのは参照1。しかし指す先の中⾝ は、もうフレーム31になっている。 再⽣すべきデータが上書きされた結果、早送りに聞こえた 94
  52. 対処:バッファを持たない⾃作ラッパー fun <T> Flow<T>.shareInWithNoExtraBuffer( scope: CoroutineScope, ): SharedFlow<T> { val

    shared = MutableSharedFlow<T>( replay = 0, extraBufferCapacity = 0, ) scope.launch { collect { shared.emit(it) } } return shared } emit が全Subscriberの消費を待つようになるので、 上流は先へ進めない 95
  53. 上書きが起きなくなる SharedFlow のバッファ: 0 — 参照を置く場所がない Producer • 書き込み位置 1

    Consumer emit 中 — 受け取られるまで返らない • 読み出し位置 Step 1 — emit が⽌まる リングバッファ ⼀周 = 30スロット バッファが 0 なので、emit は Consumer が受け取る まで返らない。Producer は次のスロットへ進めな い。 96
  54. 上書きが起きなくなる SharedFlow のバッファ: 0 — 参照を置く場所がない Producer • 書き込み位置 2

    Consumer emit 中 — 受け取られるまで返らない • 読み出し位置 Step 2 — 1つ進んで、また⽌まる リングバッファ ⼀周 = 30スロット Consumer が受け取ると emit が返り、Producer は次 の1つだけ書ける。ふたつの位置は隣り合ったまま離 れない。 97
  55. まとめ • ⾃分が書いた Flow は cold か hot か ◦

    hot なら、バッファがいくつあるかを意識する • ⾳声処理でリングバッファがある処理をSharedFlowで扱う場合、バッファ数に気 をつける。 ◦ スロット数より⼤きいと、読まれる前に上書きされる場合がある。 Flowをなんとなくで使わず、バッファリングまで含めて理解して使うことで 複雑な処理を安全に扱うことができます。 102