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

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

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

GREE Tech Conference 2026で発表された資料です。
https://techcon.gree.jp/2026/session/TrackC-4

Avatar for gree_tech

gree_tech PRO

October 16, 2026

More Decks by gree_tech

Other Decks in Technology

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. Consumer:端末で⾳声を再⽣する override suspend fun consume(data:DualChannelAudioBuffer) { val stereoBuffer = data.stereoBuffer

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

    // 再生用 audioTrack.write( stereoBuffer, stereoBuffer.remaining(), AudioTrack.WRITE_BLOCKING) 36
  17. パイプラインの組み⽴て 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のラッパー 37
  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のラッパー 38
  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のラッパー 39
  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のラッパー 40
  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のラッパー 41
  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のラッパー 42
  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のラッパー 43
  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のラッパー 44
  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) } // 再生へ 46
  26. Flowは cold と hot に分かれる 比較項目 Cold Flow Hot Flow

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

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

    Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ) バッファの⼤きさと、あふれたときの挙動を、作るときに指定できる 57
  29. バッファなし: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") } 58
  30. バッファなし:Producer が直接 Consumer に 渡す Producer (送信) Consumer (受信) A,B,C

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

    Step 1 Producer A バッファ — Consumer A を受信 Step 2 Step 3 Step 4 Step 1 Producer が A を送信。Consumer が直接 A を受け取って処理を開始します。 60
  32. バッファなし - 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 は即座にサスペンドします。 61
  33. バッファなし - 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 送信が完了します。 62
  34. バッファなし - 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 を処理中のため再びサスペンド。バッファが無いと毎回⾜⽌めされま す。 63
  35. バッファあり: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") } 64
  36. バッファあり - 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 を受け取って処理を開始します(バッファは空のまま)。 66
  37. バッファあり - 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 は⽌まらず先へ進めます。 67
  38. バッファあり - 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個)に達して満杯になります。 68
  39. バッファあり - 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 を処理 中です。 69
  40. バッファあり - 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 が⼊り、サスペンドが解けます。 70
  41. バッファあり - 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 ] で再び満杯。ふたたびサスペンドします。 71
  42. バッファの⼤きさ = 読まれていない値を溜めておける数 Producer Consumer バッファ 0 Consumerが処理を終えるまでProducerは次の値をEmitできない Producer Consumer

    バッファ 2 バッファが満杯になるまでProducerは次の値をEmitできる。 バッファが満杯になればProducerはバッファが空くまで次の値を Emitできない。 72