7 changed files with 342 additions and 162 deletions
@ -0,0 +1,100 @@ |
|||
package com.dzecevic.babymonitor |
|||
|
|||
import android.media.AudioFormat |
|||
import android.media.AudioRecord |
|||
import android.media.MediaRecorder |
|||
import android.media.audiofx.AcousticEchoCanceler |
|||
import android.media.audiofx.AutomaticGainControl |
|||
import android.media.audiofx.NoiseSuppressor |
|||
import android.util.Log |
|||
import java.util.concurrent.atomic.AtomicBoolean |
|||
|
|||
class AudioRecorder( |
|||
private val sampleRate: Int = 16000 |
|||
) { |
|||
|
|||
private var record: AudioRecord? = null |
|||
private val running = AtomicBoolean(false) |
|||
private var thread: Thread? = null |
|||
|
|||
fun start() { |
|||
if (running.getAndSet(true)) return |
|||
|
|||
val minBuf = AudioRecord.getMinBufferSize( |
|||
sampleRate, |
|||
AudioFormat.CHANNEL_IN_MONO, |
|||
AudioFormat.ENCODING_PCM_16BIT |
|||
) |
|||
|
|||
// Target ~40ms buffer (low latency but stable) |
|||
val bufSize = (minBuf * 2).coerceAtLeast(sampleRate / 25 * 2) |
|||
|
|||
record = AudioRecord( |
|||
MediaRecorder.AudioSource.VOICE_COMMUNICATION, // MUCH better than MIC |
|||
sampleRate, |
|||
AudioFormat.CHANNEL_IN_MONO, |
|||
AudioFormat.ENCODING_PCM_16BIT, |
|||
bufSize |
|||
) |
|||
|
|||
val r = record ?: return |
|||
|
|||
if (r.state != AudioRecord.STATE_INITIALIZED) { |
|||
Log.e("AudioRecorder", "AudioRecord not initialized") |
|||
running.set(false) |
|||
return |
|||
} |
|||
|
|||
r.startRecording() |
|||
|
|||
// Disable latency-causing DSP if possible |
|||
try { |
|||
NoiseSuppressor.create(r.audioSessionId)?.enabled = false |
|||
AutomaticGainControl.create(r.audioSessionId)?.enabled = false |
|||
AcousticEchoCanceler.create(r.audioSessionId)?.enabled = false |
|||
} catch (_: Exception) {} |
|||
|
|||
thread = Thread { |
|||
val buf = ByteArray(1024) // small chunk = low latency |
|||
|
|||
while (running.get()) { |
|||
val n = r.read(buf, 0, buf.size) |
|||
|
|||
if (n > 0) { |
|||
// Optional debug level meter |
|||
/* |
|||
var max = 0 |
|||
var i = 0 |
|||
while (i + 1 < n) { |
|||
val lo = buf[i].toInt() and 0xff |
|||
val hi = buf[i + 1].toInt() |
|||
val sample = (hi shl 8) or lo |
|||
max = max.coerceAtLeast(kotlin.math.abs(sample)) |
|||
i += 2 |
|||
} |
|||
Log.d("AUDIO", "level=$max") |
|||
*/ |
|||
|
|||
FrameStoreAudio.latestPcm.set(buf.copyOf(n)) |
|||
} |
|||
} |
|||
}.apply { |
|||
isDaemon = true |
|||
start() |
|||
} |
|||
} |
|||
|
|||
fun stop() { |
|||
running.set(false) |
|||
|
|||
try { thread?.join(300) } catch (_: Exception) {} |
|||
thread = null |
|||
|
|||
try { |
|||
record?.stop() |
|||
record?.release() |
|||
} catch (_: Exception) {} |
|||
|
|||
record = null |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
package com.dzecevic.babymonitor |
|||
|
|||
import java.util.concurrent.LinkedBlockingQueue |
|||
|
|||
object AudioStore { |
|||
val audioQueue = LinkedBlockingQueue<ByteArray>(50) |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
package com.dzecevic.babymonitor |
|||
|
|||
import java.util.concurrent.atomic.AtomicReference |
|||
|
|||
object FrameStoreAudio { |
|||
val latestPcm = AtomicReference<ByteArray?>(null) |
|||
} |
|||
Loading…
Reference in new issue