// Copyright (c) 2026, CIIL Anuvani-LID. Apache-2.0. // // Live language identification from the microphone: AudioRecord (16 kHz mono float) -> sherpa-onnx's Silero VAD -> // each finished stretch of speech is classified on a worker thread (so a slow classification never stalls the // recording) -> onSegment(start, end, scores) on that worker thread. Needs android.permission.RECORD_AUDIO. // A starting point: copy it into your app and change the package (docs/03-android.md, "Live microphone"). package com.example.anuvanilid import android.annotation.SuppressLint import android.media.AudioFormat import android.media.AudioRecord import android.media.MediaRecorder import com.k2fsa.sherpa.onnx.SileroVadModelConfig import com.k2fsa.sherpa.onnx.Vad import com.k2fsa.sherpa.onnx.VadModelConfig import java.util.concurrent.Executors import java.util.concurrent.TimeUnit class LiveLanguageId( private val identifier: LanguageIdentifier, vadModel: String, /** Called once per stretch of speech (seconds from start(); the most likely languages first). */ private val onSegment: (startS: Double, endS: Double, scores: List) -> Unit, /** Longest stretch passed to the model in one piece (seconds). */ maxSpeechSeconds: Float = 15f, ) : AutoCloseable { private val sampleRate = 16000 private val vad = Vad( config = VadModelConfig( sileroVadModelConfig = SileroVadModelConfig( model = vadModel, threshold = 0.5f, minSilenceDuration = 0.3f, minSpeechDuration = 0.25f, windowSize = 512, maxSpeechDuration = maxSpeechSeconds, ), sampleRate = sampleRate, numThreads = 1, ), ) private val worker = Executors.newSingleThreadExecutor() @Volatile private var running = false private var thread: Thread? = null @SuppressLint("MissingPermission") fun start() { check(!running) { "already started" } running = true thread = Thread { val minBuf = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT) val rec = AudioRecord( MediaRecorder.AudioSource.VOICE_RECOGNITION, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT, maxOf(minBuf, 4 * sampleRate), ) val buf = FloatArray(512) rec.startRecording() while (running) { val n = rec.read(buf, 0, buf.size, AudioRecord.READ_BLOCKING) if (n <= 0) continue vad.acceptWaveform(if (n == buf.size) buf.copyOf() else buf.copyOf(n)) drain() } rec.stop() rec.release() vad.flush() drain() }.apply { name = "anuvani-lid-mic"; start() } } /** Stop recording; segments already queued are still classified. */ fun stop() { running = false thread?.join() thread = null } override fun close() { stop() worker.shutdown() worker.awaitTermination(60, TimeUnit.SECONDS) vad.release() } private fun drain() { while (!vad.empty()) { val seg = vad.front() vad.pop() val start = seg.start / sampleRate.toDouble() val end = (seg.start + seg.samples.size) / sampleRate.toDouble() val samples = seg.samples worker.execute { onSegment(start, end, identifier.classify(samples, sampleRate, topK = 3)) } } } }