Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.example.appfunctions.agent.data
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.appfunctions.metadata.AppFunctionMetadata
import com.example.appfunctions.agent.domain.LiveAudioConfig
import com.example.appfunctions.agent.domain.LiveLlmEvent
import com.example.appfunctions.agent.domain.LiveLlmProvider
import com.example.appfunctions.agent.domain.LiveLlmSession
Expand Down Expand Up @@ -130,6 +131,12 @@ class GeminiLiveProviderImpl
tools: List<AppFunctionMetadata>,
): LiveLlmSession =
object : LiveLlmSession {
override val audioConfig: LiveAudioConfig =
LiveAudioConfig(
inputSampleRateHz = DEFAULT_INPUT_SAMPLE_RATE_HZ,
outputSampleRateHz = DEFAULT_OUTPUT_SAMPLE_RATE_HZ,
)

override val events: Flow<LiveLlmEvent> =
flow {
while (currentCoroutineContext().isActive && webSocketSession.isActive) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package com.example.appfunctions.agent.di

import android.content.Context
import android.media.AudioManager
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import androidx.room.Room
import com.example.appfunctions.agent.data.AndroidLiveAudioSession
import com.example.appfunctions.agent.data.ChatRepository
import com.example.appfunctions.agent.data.DataStoreSettingsRepository
import com.example.appfunctions.agent.data.FileDownloadHelper
Expand All @@ -31,6 +33,7 @@ import com.example.appfunctions.agent.data.RoomChatRepository
import com.example.appfunctions.agent.data.SettingsRepository
import com.example.appfunctions.agent.data.db.AppDatabase
import com.example.appfunctions.agent.data.db.dao.ChatDao
import com.example.appfunctions.agent.domain.LiveAudioSessionFactory
import com.example.appfunctions.agent.domain.LlmProviderFactory
import dagger.Binds
import dagger.Module
Expand Down Expand Up @@ -120,6 +123,19 @@ abstract class DataModule {
}
}

@Provides
@Singleton
fun provideLiveAudioSessionFactory(
@ApplicationContext context: Context,
): LiveAudioSessionFactory {
return LiveAudioSessionFactory { config ->
AndroidLiveAudioSession(
config = config,
audioManager = context.getSystemService(AudioManager::class.java),
)
}
}

private const val SOCKET_TIMEOUT_MS = 30_000L
private const val WEBSOCKET_PING_INTERVAL_MS = 20_000L
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.appfunctions.agent.domain

import android.Manifest
import androidx.annotation.RequiresPermission
import kotlinx.coroutines.flow.Flow

/**
* Provider-supplied PCM audio sample rate configuration for a [LiveAudioSession].
*
* @property inputSampleRateHz Microphone recording sample rate in Hz.
* @property outputSampleRateHz Default assistant playback sample rate in Hz.
*/
data class LiveAudioConfig(
val inputSampleRateHz: Int,
val outputSampleRateHz: Int,
) {
init {
require(inputSampleRateHz > 0) { "inputSampleRateHz must be positive: $inputSampleRateHz" }
require(outputSampleRateHz > 0) { "outputSampleRateHz must be positive: $outputSampleRateHz" }
}
}

/** Abstraction over full-duplex audio recording and playback for a live voice session. */
interface LiveAudioSession {
/** PCM microphone capture sample rate in Hz emitted by [listenToRecording]. */
val inputSampleRateHz: Int

/** Acquires audio focus/routing and starts microphone capture and audio playback resources. */
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
fun start()

/** Streams captured PCM microphone audio chunks while recording is active and unpaused. */
fun listenToRecording(): Flow<ByteArray>

/** Temporarily suppresses microphone emissions (e.g. while executing tools). */
fun pauseRecording()

/** Resumes microphone emissions after [pauseRecording]. */
fun resumeRecording()

/**
* Enqueues a PCM audio chunk from the assistant for speaker/headset playback.
*
* @param pcmBytes Raw 16-bit little-endian mono PCM audio bytes.
* @param sampleRateHz Playback sample rate in Hz.
*/
fun queueAudio(
pcmBytes: ByteArray,
sampleRateHz: Int,
)

/** Immediately clears queued assistant audio and flushes active playback on interruption. */
fun stopAudio()

/** Releases all underlying audio recording, playback, and audio-focus resources. */
fun release()
}

/** Factory for creating a new [LiveAudioSession] configured with [LiveAudioConfig] per voice session. */
fun interface LiveAudioSessionFactory {
fun create(config: LiveAudioConfig): LiveAudioSession
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ suspend inline fun LiveLlmProvider.withSession(

/** Represents an active bidirectional voice and tool session with an LLM provider. */
interface LiveLlmSession {
/** Audio capture and playback sample rate configuration required by this session's provider. */
val audioConfig: LiveAudioConfig

/** Sequential stream of server-originated events for this live session. */
val events: Flow<LiveLlmEvent>

Expand Down
Loading
Loading