Anuvani-LID / mobile_inference /android /LiveLanguageId.kt
StephennFernandes's picture
CIIL Anuvani-LID
bd48fd1
Raw History Blame Contribute Delete
3.66 kB
// 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<LanguageScore>) -> 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)) }
}
}
}