Build, maintain, and extend the EarLLM One Android project — a Kotlin/Compose app that connects Bluetooth earbuds to an LLM via voice pipeline.
EarLLM One is a multi-module Android app (Kotlin + Jetpack Compose) that captures voice from Bluetooth earbuds, transcribes it, sends it to an LLM, and speaks the response back.
C:\Users\renat\earbudllm
app ──→ voice ──→ audio ──→ core-logging
│ │
├──→ bluetooth ──→ core-logging
└──→ llm ──→ core-logging
| Module | Purpose | Key Files |
|---|---|---|
| core-logging | Structured logging, performance tracking | EarLogger.kt, PerformanceTracker.kt |
| bluetooth | BT discovery, pairing, A2DP/HFP profiles | BluetoothController.kt, BluetoothState.kt, BluetoothPermissions.kt |
| audio | Audio routing (SCO/BLE), capture, headset buttons | AudioRouteController.kt, VoiceCaptureController.kt, HeadsetButtonController.kt |
| voice | STT (SpeechRecognizer + Vosk stub), TTS, pipeline | SpeechToTextController.kt, TextToSpeechController.kt, VoicePipeline.kt |
| llm | LLM interface, stub, OpenAI-compatible client | LlmClient.kt, StubLlmClient.kt, RealLlmClient.kt, SecureTokenStore.kt |
| app | UI, ViewModel, Service, Settings, all screens | MainViewModel.kt, EarLlmForegroundService.kt, 6 Compose screens |
| Device | Model | Key Details |
|---|---|---|
| Phone | Samsung Galaxy S24 Ultra | Android 14, One UI 6.1, Snapdragon 8 Gen 3 |
| Earbuds | Xiaomi Redmi Buds 6 Pro | BT 5.3, A2DP/HFP/AVRCP, ANC, LDAC |
These are verified facts from official documentation and device testing. Treat them as ground truth when making decisions:
Bluetooth SCO is limited to 8kHz mono input on most devices. Some support 16kHz mSBC. BLE Audio (Android 12+, TYPE_BLE_HEADSET = 26) supports up to 32kHz stereo. Always prefer BLE Audio when available.
startBluetoothSco() is deprecated since Android 12 (API 31). Use AudioManager.setCommunicationDevice(AudioDeviceInfo) and clearCommunicationDevice() instead. The project already implements both paths in AudioRouteController.kt.
Samsung One UI 7/8 has a known HFP corruption bug where A2DP playback corrupts the SCO link. The app handles this with silence detection and automatic fallback to the phone's built-in mic.
Redmi Buds 6 Pro tap controls must be set to "Default" (Play/Pause) in the Xiaomi Earbuds companion app. If set to ANC or custom functions, events are handled internally by the earbuds and never reach Android.
Android 14+ requires FOREGROUND_SERVICE_MICROPHONE permission and foregroundServiceType="microphone" in the service declaration. RECORD_AUDIO must be granted before startForeground().
VOICE_COMMUNICATION audio source enables AEC (Acoustic Echo Cancellation), which is critical to prevent TTS audio output from feeding back into the STT microphone input. Never change this source without understanding the echo implications.
Never play TTS (A2DP) while simultaneously recording via SCO. The correct sequence is: stop playback → switch to HFP → record → switch to A2DP → play response.
Headset button tap
→ MediaSession (HeadsetButtonController)
→ TapAction.RECORD_TOGGLE
→ VoicePipeline.toggleRecording()
→ VoiceCaptureController captures PCM (16kHz mono)
→ stopRecording() returns ByteArray
→ SpeechToTextController.transcribe(pcmData)
→ LlmClient.chat(messages)
→ TextToSpeechController.speak(response)
→ Audio output via A2DP to earbuds
MutableStateFlow / StateFlow
MainViewModel.kt if the feature needs UI integrationsrc/test/ directoryVoiceCaptureController.kt handles PCM recording at 16kHz monogetMinBufferSize().coerceAtLeast(4096)
BluetoothController.kt manages discovery, pairing, profile proxiesLlmClient.kt defines the interface — keep it genericStubLlmClient.kt for offline testing (500ms simulated delay)RealLlmClient.kt uses OkHttp to call OpenAI-compatible APIsSecureTokenStore.kt (EncryptedSharedPreferences)After code changes, regenerate the ZIP:
## From Project Root
powershell -Command "Remove-Item 'EarLLM_One_v1.0.zip' -Force -ErrorAction SilentlyContinue; Compress-Archive -Path (Get-ChildItem -Exclude '*.zip','_zip_verify','.git') -DestinationPath 'EarLLM_One_v1.0.zip' -Force"
./gradlew test --stacktrace # Unit tests
./gradlew connectedAndroidTest # Instrumented tests (device required)
| Engine | Size | WER | Streaming | Best For |
|---|---|---|---|---|
| Vosk small-en | 40 MB | ~10% | Yes | Real-time mobile |
| Vosk lgraph | 128 MB | ~8% | Yes | Better accuracy |
| Whisper tiny | 40 MB | ~10-12% | No (batch) | Post-utterance polish |
| Android SpeechRecognizer | 0 MB | varies | Yes | Online, no extra deps |