@@ -29,18 +29,18 @@ private const val TAG = "AndroidCodec2Encoder"
2929/* *
3030 * Android implementation of [Codec2Encoder].
3131 *
32- * When [Codec2Jni .isAvailable] = true, uses libcodec2 via JNI (real voice audio).
32+ * When [Codec2JNI .isAvailable] is true, uses libcodec2 via JNI (real voice audio).
3333 * Otherwise falls back to STUB mode (440Hz sine wave) for development/CI/builds without .so.
3434 *
3535 * Codec2 700B parameters:
3636 * - Sample rate input: 8000 Hz
3737 * - Frame: 40ms = 320 samples
3838 * - Bytes per frame: 4
39- * - 1 second: 25 frames × 4 bytes = 100 bytes
39+ * - 1 second: 25 frames x 4 bytes = 100 bytes
4040 *
4141 * Preprocessing applied before encoding (JNI mode only):
4242 * 1. Amplitude normalization (brings to 70% of Short.MAX_VALUE)
43- * 2. Simple VAD: if RMS < threshold, returns silence without encoding
43+ * 2. Simple VAD: if RMS < threshold, returns null without encoding
4444 *
4545 * JNI Lifecycle:
4646 * The Codec2 handle is created in the constructor and destroyed in [close()].
@@ -63,14 +63,14 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
6363 " bytesPerFrame=${Codec2JNI .getBytesPerFrame(Codec2JNI .MODE_700C )} "
6464 }
6565 } else {
66- Logger .e(tag = TAG ) { " Codec2JNI.create() returned 0 — falling back to stub mode" }
66+ Logger .e(tag = TAG ) { " Codec2JNI.create() returned 0 -- falling back to stub mode" }
6767 codec2Handle = 0L
6868 isStub = true
6969 }
7070 } else {
7171 codec2Handle = 0L
7272 isStub = true
73- Logger .w(tag = TAG ) { " Codec2 JNI not available — stub mode (440Hz sine wave)" }
73+ Logger .w(tag = TAG ) { " Codec2 JNI not available -- stub mode (440Hz sine wave)" }
7474 }
7575 }
7676
@@ -81,17 +81,17 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
8181 }
8282 }
8383
84- // ─── encode ───────────────────────────────────────────────────────────────
84+ // --- encode -------------------------------------------------------------
8585
8686 /* *
8787 * Encodes 16-bit mono 8000Hz PCM into Codec2 700B bytes.
8888 *
89- * Accepts an array of any length — it is split into frames
89+ * Accepts an array of any length -- it is split into frames
9090 * of [SAMPLES_PER_FRAME] samples. The last incomplete frame is
9191 * padded with zeros (zero-padding).
9292 *
9393 * @param pcmData PCM samples from the microphone (8000 Hz, mono, signed 16-bit)
94- * @return ByteArray with Codec2 bytes, null if input is empty
94+ * @return ByteArray with Codec2 bytes, null if input is empty or silence detected
9595 */
9696 override fun encode (pcmData : ShortArray ): ByteArray? {
9797 if (pcmData.isEmpty()) return null
@@ -110,11 +110,11 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
110110 // Preprocessing: normalization
111111 val normalized = normalize(pcmData)
112112
113- // VAD: do not send silence
113+ // VAD: do not send silence -- return null so the ViewModel skips transmission
114114 val rms = computeRms(normalized)
115115 if (rms < SILENCE_RMS_THRESHOLD ) {
116- Logger .d(tag = TAG ) { " VAD: silence detected (RMS=$rms ) — skipping encode" }
117- return ByteArray ( 0 )
116+ Logger .d(tag = TAG ) { " VAD: silence detected (RMS=$rms ) -- skipping encode" }
117+ return null
118118 }
119119
120120 // Calculate needed frames (round up)
@@ -132,7 +132,6 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
132132 } else {
133133 ShortArray (samplesPerFrame).also {
134134 normalized.copyInto(it, 0 , inStart, inEnd)
135- // remaining already 0 by default
136135 }
137136 }
138137
@@ -147,13 +146,13 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
147146 }
148147
149148 Logger .d(tag = TAG ) {
150- " Encode JNI: ${pcmData.size} samples → ${output.size} bytes " +
151- " ($frameCount frames × $bytesPerFrame bytes)"
149+ " Encode JNI: ${pcmData.size} samples -> ${output.size} bytes " +
150+ " ($frameCount frames x $bytesPerFrame bytes)"
152151 }
153152 return output
154153 }
155154
156- // ─── decode ───────────────────────────────────────────────────────────────
155+ // --- decode -------------------------------------------------------------
157156
158157 /* *
159158 * Decodes Codec2 700B bytes into 16-bit mono 8000Hz PCM samples.
@@ -178,7 +177,7 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
178177 if (codec2Data.size % bytesPerFrame != 0 ) {
179178 Logger .w(tag = TAG ) {
180179 " Decode: input size (${codec2Data.size} ) not a multiple of " +
181- " bytesPerFrame ($bytesPerFrame ) — truncating to complete frame"
180+ " bytesPerFrame ($bytesPerFrame ) -- truncating to complete frame"
182181 }
183182 }
184183
@@ -203,24 +202,23 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
203202 }
204203
205204 Logger .d(tag = TAG ) {
206- " Decode JNI: ${codec2Data.size} bytes → ${output.size} samples " +
207- " ($frameCount frames × $samplesPerFrame samples)"
205+ " Decode JNI: ${codec2Data.size} bytes -> ${output.size} samples " +
206+ " ($frameCount frames x $samplesPerFrame samples)"
208207 }
209208 return output
210209 }
211210
212- // ─── Preprocessing helpers ────────────────────────────────────────────────
211+ // --- Preprocessing helpers ----------------------------------------------
213212
214213 /* *
215- * Normalizes the signal amplitude to [TARGET_AMPLITUDE] × Short.MAX_VALUE.
214+ * Normalizes the signal amplitude to [TARGET_AMPLITUDE] x Short.MAX_VALUE.
216215 * Prevents clipping and improves Codec2 quality on low-volume voices.
217216 */
218217 private fun normalize (pcm : ShortArray ): ShortArray {
219218 val maxAmp = pcm.maxOfOrNull { abs(it.toInt()) }?.toFloat() ? : return pcm
220219 if (maxAmp < 1f ) return pcm // absolute silence
221220
222221 val gain = (TARGET_AMPLITUDE * Short .MAX_VALUE ) / maxAmp
223- // Limit maximum gain to 10x to avoid excessive noise amplification
224222 val clampedGain = minOf(gain, MAX_GAIN )
225223
226224 return ShortArray (pcm.size) { i ->
@@ -238,12 +236,12 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
238236 return sqrt(sumSquares / pcm.size)
239237 }
240238
241- // ─── Stub (fallback when JNI is not available) ─────────────────────────
239+ // --- Stub (fallback when JNI is not available) --------------------------
242240
243241 private fun encodeStub (pcmData : ShortArray ): ByteArray {
244242 val frameCount = (pcmData.size + SAMPLES_PER_FRAME - 1 ) / SAMPLES_PER_FRAME
245243 Logger .w(tag = TAG ) {
246- " Codec2 STUB encode: ${pcmData.size} samples → ${frameCount * BYTES_PER_FRAME } bytes (zeros)"
244+ " Codec2 STUB encode: ${pcmData.size} samples -> ${frameCount * BYTES_PER_FRAME } bytes (zeros)"
247245 }
248246 return ByteArray (frameCount * BYTES_PER_FRAME ) { 0x00 }
249247 }
@@ -253,10 +251,10 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
253251 val totalSamples = frameCount * SAMPLES_PER_FRAME
254252
255253 Logger .w(tag = TAG ) {
256- " Codec2 STUB decode: ${codec2Data.size} bytes → $totalSamples samples (440Hz sine wave)"
254+ " Codec2 STUB decode: ${codec2Data.size} bytes -> $totalSamples samples (440Hz sine wave)"
257255 }
258256
259- // Generate 440Hz sine wave (A4) — audible and recognizable
257+ // Generate 440Hz sine wave (A4) -- audible and recognizable
260258 val sampleRate = 8000.0
261259 val frequency = 440.0
262260 val amplitude = Short .MAX_VALUE * 0.3 // 30% volume
@@ -277,12 +275,12 @@ class AndroidCodec2Encoder : Codec2Encoder, AutoCloseable {
277275 /* * Target amplitude for normalization (70% of Short.MAX_VALUE). */
278276 private const val TARGET_AMPLITUDE = 0.70f
279277
280- /* * Maximum gain applied by normalization (10× ). */
278+ /* * Maximum gain applied by normalization (10x ). */
281279 private const val MAX_GAIN = 10.0f
282280
283281 /* *
284282 * RMS threshold below which the frame is considered silence (simple VAD).
285- * 200.0 on the 0-32767 scale is approximately -44 dBFS — normal voice is 2000-8000.
283+ * 200.0 on the 0-32767 scale is approximately -44 dBFS -- normal voice is 2000-8000.
286284 */
287285 private const val SILENCE_RMS_THRESHOLD = 200.0
288286 }
0 commit comments