1package com.peachpasswords.sync23import android.annotation.SuppressLint4import android.media.AudioAttributes5import android.media.AudioFormat6import android.media.AudioManager7import android.media.AudioRecord8import android.media.AudioTrack9import android.media.MediaRecorder10import kotlinx.coroutines.Dispatchers11import kotlinx.coroutines.withContext1213141516171819202122232425private const val SAMPLE_RATE = 48_0002627@SuppressLint("MissingPermission") 28suspend fun openPcmFloatAudioIO(): AudioIO = withContext(Dispatchers.IO) {29 val minIn = AudioRecord.getMinBufferSize(30 SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_FLOAT,31 )32 if (minIn <= 0) {33 throw ChirpExchangeError(ChirpExchangeErrorKind.AUDIO_UNAVAILABLE, "mic buffer size unavailable")34 }35 val recordBuffer = maxOf(minIn, SAMPLE_RATE / 4 * 4)36 val record = try {37 AudioRecord(38 MediaRecorder.AudioSource.MIC,39 SAMPLE_RATE,40 AudioFormat.CHANNEL_IN_MONO,41 AudioFormat.ENCODING_PCM_FLOAT,42 recordBuffer,43 )44 } catch (e: Exception) {45 throw ChirpExchangeError(ChirpExchangeErrorKind.MIC_DENIED, e.message ?: "mic init failed")46 }47 if (record.state != AudioRecord.STATE_INITIALIZED) {48 record.release()49 throw ChirpExchangeError(ChirpExchangeErrorKind.MIC_DENIED, "mic failed to initialise")50 }5152 val minOut = AudioTrack.getMinBufferSize(53 SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_FLOAT,54 )55 val track = AudioTrack.Builder()56 .setAudioAttributes(57 AudioAttributes.Builder()58 .setUsage(AudioAttributes.USAGE_MEDIA)59 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)60 .build(),61 )62 .setAudioFormat(63 AudioFormat.Builder()64 .setSampleRate(SAMPLE_RATE)65 .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)66 .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)67 .build(),68 )69 .setBufferSizeInBytes(maxOf(minOut, 4096))70 .setTransferMode(AudioTrack.MODE_STREAM)71 .build()7273 record.startRecording()7475 object : AudioIO {76 override val sampleRate = SAMPLE_RATE77 private val readBuf = FloatArray(recordBuffer / 4)7879 override suspend fun play(waveform: FloatArray) = withContext(Dispatchers.IO) {80 if (track.playState != AudioTrack.PLAYSTATE_PLAYING) track.play()81 var offset = 082 while (offset < waveform.size) {83 val n = track.write(waveform, offset, waveform.size - offset, AudioTrack.WRITE_BLOCKING)84 if (n < 0) break85 offset += n86 }87 }8889 override suspend fun pullSamples(): FloatArray = withContext(Dispatchers.IO) {90 val n = record.read(readBuf, 0, readBuf.size, AudioRecord.READ_BLOCKING)91 if (n <= 0) FloatArray(0) else readBuf.copyOf(n)92 }9394 override suspend fun close(): Unit = withContext(Dispatchers.IO) {95 runCatching { record.stop() }96 runCatching { record.release() }97 runCatching { track.stop() }98 runCatching { track.release() }99 Unit100 }101 }102}