MainActivity.kt 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. package com.example.flutter_audio_record
  2. import android.Manifest
  3. import android.content.pm.PackageManager
  4. import android.media.AudioFormat
  5. import android.media.AudioRecord
  6. import android.media.MediaRecorder
  7. import android.os.Build
  8. import android.util.Log
  9. import androidx.annotation.NonNull
  10. import androidx.core.app.ActivityCompat
  11. import androidx.core.content.ContextCompat
  12. import io.flutter.embedding.android.FlutterActivity
  13. import io.flutter.embedding.engine.FlutterEngine
  14. import io.flutter.plugin.common.MethodCall
  15. import io.flutter.plugin.common.MethodChannel
  16. import io.flutter.plugin.common.MethodChannel.Result
  17. import io.flutter.plugin.common.PluginRegistry
  18. import java.io.*
  19. import java.nio.ByteBuffer
  20. import java.nio.ByteOrder
  21. import java.util.*
  22. class MainActivity: FlutterActivity() {
  23. private val LOG_NAME = "AndroidAudioRecorder"
  24. private val PERMISSIONS_REQUEST_RECORD_AUDIO = 200
  25. private val RECORDER_BPP: Byte = 16 // we use 16bit
  26. private val registrar: PluginRegistry.Registrar? = null
  27. private var mSampleRate = 16000 // 16Khz
  28. private var mRecorder: AudioRecord? = null
  29. private var mFilePath: String? = null
  30. private var mExtension: String? = null
  31. private var bufferSize = 1024
  32. private var mFileOutputStream: FileOutputStream? = null
  33. private var mStatus = "unset"
  34. private var mPeakPower = -120.0
  35. private var mAveragePower = -120.0
  36. private var mRecordingThread: Thread? = null
  37. private var mDataSize: Long = 0
  38. private var _result: Result? = null
  39. private val CHANNEL: String? ="flutter_audio_recorder2"
  40. override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
  41. super.configureFlutterEngine(flutterEngine)
  42. MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
  43. call, result ->
  44. _result = result
  45. when (call.method) {
  46. "hasPermissions" -> handleHasPermission()
  47. "init" -> handleInit(call, result)
  48. "current" -> handleCurrent(call, result)
  49. "start" -> handleStart(call, result)
  50. "pause" -> handlePause(call, result)
  51. "resume" -> handleResume(call, result)
  52. "stop" -> handleStop(call, result)
  53. else -> result.notImplemented()
  54. }
  55. }
  56. }
  57. fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>?, grantResults: IntArray): Boolean {
  58. val REQUEST_RECORD_AUDIO_PERMISSION = 200
  59. return when (requestCode) {
  60. REQUEST_RECORD_AUDIO_PERMISSION -> {
  61. var granted = true
  62. Log.d(LOG_NAME, "parsing result")
  63. for (result in grantResults) {
  64. if (result != PackageManager.PERMISSION_GRANTED) {
  65. Log.d(LOG_NAME, "result$result")
  66. granted = false
  67. }
  68. }
  69. Log.d(LOG_NAME, "onRequestPermissionsResult -$granted")
  70. if (_result != null) {
  71. _result!!.success(granted)
  72. }
  73. granted
  74. }
  75. else -> {
  76. Log.d(LOG_NAME, "onRequestPermissionsResult - false")
  77. false
  78. }
  79. }
  80. }
  81. private fun hasRecordPermission(): Boolean {
  82. // if after [Marshmallow], we need to check permission on runtime
  83. return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  84. (registrar?.let { ContextCompat.checkSelfPermission(it.context(), Manifest.permission.RECORD_AUDIO) } === PackageManager.PERMISSION_GRANTED
  85. && registrar?.let { ContextCompat.checkSelfPermission(it.context(), Manifest.permission.WRITE_EXTERNAL_STORAGE) } === PackageManager.PERMISSION_GRANTED)
  86. } else {
  87. registrar?.let { ContextCompat.checkSelfPermission(it.context(), Manifest.permission.RECORD_AUDIO) } === PackageManager.PERMISSION_GRANTED
  88. }
  89. }
  90. private fun handleHasPermission() {
  91. if (hasRecordPermission()) {
  92. Log.d(LOG_NAME, "handleHasPermission true")
  93. if (_result != null) {
  94. _result!!.success(true)
  95. }
  96. } else {
  97. Log.d(LOG_NAME, "handleHasPermission false")
  98. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
  99. registrar?.let { ActivityCompat.requestPermissions(it.activity(), arrayOf<String>(Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_RECORD_AUDIO) }
  100. } else {
  101. registrar?.let { ActivityCompat.requestPermissions(it.activity(), arrayOf<String>(Manifest.permission.RECORD_AUDIO), PERMISSIONS_REQUEST_RECORD_AUDIO) }
  102. }
  103. }
  104. }
  105. private fun handleInit(call: MethodCall, result: Result) {
  106. resetRecorder()
  107. mSampleRate = Integer.parseInt(call.argument("sampleRate"))
  108. mFilePath = call.argument("path")
  109. mExtension = call.argument("extension")
  110. bufferSize = AudioRecord.getMinBufferSize(mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
  111. mStatus = "initialized"
  112. val initResult: HashMap<String, Any> = HashMap()
  113. initResult.put("duration", "0")
  114. mFilePath?.let { initResult.put("path", it) }
  115. mExtension?.let { initResult.put("audioFormat", it) }
  116. initResult.put("peakPower", mPeakPower)
  117. initResult.put("averagePower", mAveragePower)
  118. initResult.put("isMeteringEnabled", true)
  119. initResult.put("status", mStatus)
  120. result.success(initResult)
  121. }
  122. private fun handleCurrent(call: MethodCall, result: Result) {
  123. val currentResult: HashMap<String, Any> = HashMap()
  124. currentResult.put("duration", getDuration() * 1000)
  125. if (mStatus === "stopped") mFilePath else getTempFilename()?.let { currentResult.put("path", it) }
  126. mExtension?.let { currentResult.put("audioFormat", it) }
  127. currentResult.put("peakPower", mPeakPower)
  128. currentResult.put("averagePower", mAveragePower)
  129. currentResult.put("isMeteringEnabled", true)
  130. currentResult.put("status", mStatus)
  131. // Log.d(LOG_NAME, currentResult.toString());
  132. result.success(currentResult)
  133. }
  134. private fun handleStart(call: MethodCall, result: Result) {
  135. mRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, mSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)
  136. try {
  137. mFileOutputStream = FileOutputStream(getTempFilename())
  138. } catch (e: FileNotFoundException) {
  139. result.error("", "cannot find the file", null)
  140. return
  141. }
  142. mRecorder!!.startRecording()
  143. mStatus = "recording"
  144. startThread()
  145. result.success(null)
  146. }
  147. private fun startThread() {
  148. mRecordingThread = Thread(Runnable {
  149. processAudioStream()
  150. },"Audio Processing Thread")
  151. mRecordingThread!!.start()
  152. }
  153. private fun handlePause(call: MethodCall, result: Result) {
  154. mStatus = "paused"
  155. mPeakPower = -120.0
  156. mAveragePower = -120.0
  157. mRecorder?.stop()
  158. mRecordingThread = null
  159. result.success(null)
  160. }
  161. private fun handleResume(call: MethodCall, result: Result) {
  162. mStatus = "recording"
  163. mRecorder?.startRecording()
  164. startThread()
  165. result.success(null)
  166. }
  167. private fun handleStop(call: MethodCall, result: Result) {
  168. if (mStatus.equals("stopped")) {
  169. result.success(null)
  170. } else {
  171. mStatus = "stopped"
  172. // Return Recording Object
  173. val currentResult: HashMap<String, Any> = HashMap()
  174. currentResult.put("duration", getDuration() * 1000)
  175. mFilePath?.let { currentResult.put("path", it) }
  176. mExtension?.let { currentResult.put("audioFormat", it) }
  177. currentResult.put("peakPower", mPeakPower)
  178. currentResult.put("averagePower", mAveragePower)
  179. currentResult.put("isMeteringEnabled", true)
  180. currentResult.put("status", mStatus)
  181. resetRecorder()
  182. mRecordingThread = null
  183. mRecorder?.stop()
  184. mRecorder!!.release()
  185. try {
  186. mFileOutputStream?.close()
  187. } catch (e: IOException) {
  188. e.printStackTrace()
  189. }
  190. Log.d(LOG_NAME, "before adding the wav header")
  191. copyWaveFile(getTempFilename(), mFilePath!!)
  192. deleteTempFile()
  193. // Log.d(LOG_NAME, currentResult.toString());
  194. result.success(currentResult)
  195. }
  196. }
  197. private fun processAudioStream() {
  198. Log.d(LOG_NAME, "processing the stream: $mStatus")
  199. val size = bufferSize
  200. val bData = ByteArray(size)
  201. while (mStatus === "recording") {
  202. Log.d(LOG_NAME, "reading audio data")
  203. mRecorder?.read(bData, 0, bData.size)
  204. mDataSize += bData.size
  205. updatePowers(bData)
  206. try {
  207. mFileOutputStream?.write(bData)
  208. } catch (e: IOException) {
  209. e.printStackTrace()
  210. }
  211. }
  212. }
  213. private fun deleteTempFile() {
  214. val file = File(getTempFilename())
  215. if (file.exists()) {
  216. file.delete()
  217. }
  218. }
  219. private fun getTempFilename(): String {
  220. return "$mFilePath.temp"
  221. }
  222. private fun copyWaveFile(inFilename: String, outFilename: String) {
  223. var `in`: FileInputStream? = null
  224. var out: FileOutputStream? = null
  225. var totalAudioLen: Long = 0
  226. var totalDataLen = totalAudioLen + 36
  227. val longSampleRate = mSampleRate.toLong()
  228. val channels = 1
  229. val byteRate = (RECORDER_BPP * mSampleRate * channels / 8).toLong()
  230. val data = ByteArray(bufferSize)
  231. try {
  232. `in` = FileInputStream(inFilename)
  233. out = FileOutputStream(outFilename)
  234. totalAudioLen = `in`.getChannel().size()
  235. totalDataLen = totalAudioLen + 36
  236. WriteWaveFileHeader(out, totalAudioLen, totalDataLen,
  237. longSampleRate, channels, byteRate)
  238. while (`in`.read(data) !== -1) {
  239. out.write(data)
  240. }
  241. `in`.close()
  242. out.close()
  243. } catch (e: FileNotFoundException) {
  244. e.printStackTrace()
  245. } catch (e: IOException) {
  246. e.printStackTrace()
  247. }
  248. }
  249. @kotlin.Throws(IOException::class)
  250. private fun WriteWaveFileHeader(out: FileOutputStream?, totalAudioLen: Long,
  251. totalDataLen: Long, longSampleRate: Long, channels: Int, byteRate: Long) {
  252. val header = ByteArray(44)
  253. header[0] = 'R'.toByte() // RIFF/WAVE header
  254. header[1] = 'I'.toByte()
  255. header[2] = 'F'.toByte()
  256. header[3] = 'F'.toByte()
  257. header[4] = (totalDataLen and 0xff).toByte()
  258. header[5] = (totalDataLen shr 8 and 0xff).toByte()
  259. header[6] = (totalDataLen shr 16 and 0xff).toByte()
  260. header[7] = (totalDataLen shr 24 and 0xff).toByte()
  261. header[8] = 'W'.toByte()
  262. header[9] = 'A'.toByte()
  263. header[10] = 'V'.toByte()
  264. header[11] = 'E'.toByte()
  265. header[12] = 'f' .toByte()// 'fmt ' chunk
  266. header[13] = 'm'.toByte()
  267. header[14] = 't'.toByte()
  268. header[15] = ' '.toByte()
  269. header[16] = 16 // 4 bytes: size of 'fmt ' chunk
  270. header[17] = 0
  271. header[18] = 0
  272. header[19] = 0
  273. header[20] = 1 // format = 1
  274. header[21] = 0
  275. header[22] = channels.toByte()
  276. header[23] = 0
  277. header[24] = (longSampleRate and 0xff).toByte()
  278. header[25] = (longSampleRate shr 8 and 0xff).toByte()
  279. header[26] = (longSampleRate shr 16 and 0xff).toByte()
  280. header[27] = (longSampleRate shr 24 and 0xff).toByte()
  281. header[28] = (byteRate and 0xff).toByte()
  282. header[29] = (byteRate shr 8 and 0xff).toByte()
  283. header[30] = (byteRate shr 16 and 0xff).toByte()
  284. header[31] = (byteRate shr 24 and 0xff).toByte()
  285. header[32] = 1.toByte() // block align
  286. header[33] = 0
  287. header[34] = RECORDER_BPP // bits per sample
  288. header[35] = 0
  289. header[36] = 'd'.toByte()
  290. header[37] = 'a'.toByte()
  291. header[38] = 't'.toByte()
  292. header[39] = 'a'.toByte()
  293. header[40] = (totalAudioLen and 0xff).toByte()
  294. header[41] = (totalAudioLen shr 8 and 0xff).toByte()
  295. header[42] = (totalAudioLen shr 16 and 0xff).toByte()
  296. header[43] = (totalAudioLen shr 24 and 0xff).toByte()
  297. if (out != null) {
  298. out.write(header, 0, 44)
  299. }
  300. }
  301. private fun byte2short(bData: ByteArray): ShortArray {
  302. val out = ShortArray(bData.size / 2)
  303. ByteBuffer.wrap(bData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(out)
  304. return out
  305. }
  306. private fun resetRecorder() {
  307. mPeakPower = -120.0
  308. mAveragePower = -120.0
  309. mDataSize = 0
  310. }
  311. private fun updatePowers(bdata: ByteArray) {
  312. val data = byte2short(bdata)
  313. val sampleVal = data[data.size - 1]
  314. val escapeStatusList = arrayOf("paused", "stopped", "initialized", "unset")
  315. mAveragePower = if (sampleVal.toInt() == 0 || Arrays.asList(escapeStatusList).contains(mStatus)) {
  316. -120.0 // to match iOS silent case
  317. } else {
  318. // iOS factor : to match iOS power level
  319. val iOSFactor = 0.25
  320. 20 * Math.log(Math.abs(sampleVal.toInt()) / 32768.0) * iOSFactor
  321. }
  322. mPeakPower = mAveragePower
  323. // Log.d(LOG_NAME, "Peak: " + mPeakPower + " average: "+ mAveragePower);
  324. }
  325. private fun getDuration(): Int {
  326. val duration = mDataSize / (mSampleRate * 2 * 1)
  327. return duration.toInt()
  328. }
  329. }