Skip to content

Commit

Permalink
add TensorAudio and recording support
Browse files Browse the repository at this point in the history
  • Loading branch information
am15h committed Jul 15, 2021
1 parent 60066ae commit f5e4d88
Show file tree
Hide file tree
Showing 8 changed files with 556 additions and 28 deletions.
2 changes: 0 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
package android

group 'com.tfliteflutter.tflite_flutter_helper'
version '1.0-SNAPSHOT'

Expand Down
1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tfliteflutter.tflite_flutter_helper">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
Original file line number Diff line number Diff line change
@@ -1,35 +1,278 @@
package com.tfliteflutter.tflite_flutter_helper

import androidx.annotation.NonNull

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.media.*
import android.media.AudioRecord.OnRecordPositionUpdateListener
import android.util.Log
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.ShortBuffer

enum class SoundStreamErrors {
FailedToRecord,
FailedToPlay,
FailedToStop,
FailedToWriteBuffer,
Unknown,
}

enum class SoundStreamStatus {
Unset,
Initialized,
Playing,
Stopped,
}

const val methodChannelName = "com.tfliteflutter.tflite_flutter_helper:methods"

/** TfliteFlutterHelperPlugin */
class TfliteFlutterHelperPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tflite_flutter_helper")
channel.setMethodCallHandler(this)
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
class TfliteFlutterHelperPlugin : FlutterPlugin,
MethodCallHandler,
PluginRegistry.RequestPermissionsResultListener,
ActivityAware {

private val logTag = "TfLiteFlutterHelperPlugin"
private val audioRecordPermissionCode = 14887

private lateinit var methodChannel: MethodChannel
private var currentActivity: Activity? = null
private var pluginContext: Context? = null
private var permissionToRecordAudio: Boolean = false
private var activeResult: Result? = null
private var debugLogging: Boolean = false

//========= Recorder's vars
private val mRecordFormat = AudioFormat.ENCODING_PCM_16BIT
private var mRecordSampleRate = 16000 // 16Khz
private var mRecorderBufferSize = 8192
private var mPeriodFrames = 8192
private var audioData: ShortArray? = null
private var mRecorder: AudioRecord? = null
private var mListener: OnRecordPositionUpdateListener? = null


override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
pluginContext = flutterPluginBinding.applicationContext
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, methodChannelName)
methodChannel.setMethodCallHandler(this)
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
try {
when (call.method) {
"hasPermission" -> hasPermission(result)
"initializeRecorder" -> initializeRecorder(call, result)
"startRecording" -> startRecording(result)
"stopRecording" -> stopRecording(result)
else -> result.notImplemented()
}
} catch (e: Exception) {
Log.e(logTag, "Unexpected exception", e)
// TODO: implement result.error
}
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
mListener?.onMarkerReached(null)
mListener?.onPeriodicNotification(null)
mListener = null
mRecorder?.stop()
mRecorder?.release()
mRecorder = null
}

override fun onDetachedFromActivity() {
// currentActivity
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
currentActivity = binding.activity
binding.addRequestPermissionsResultListener(this)
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
currentActivity = binding.activity
binding.addRequestPermissionsResultListener(this)
}

override fun onDetachedFromActivityForConfigChanges() {
// currentActivity = null
}

/** ======== Plugin methods ======== **/

private fun hasRecordPermission(): Boolean {
if (permissionToRecordAudio) return true

val localContext = pluginContext
permissionToRecordAudio = localContext != null && ContextCompat.checkSelfPermission(localContext,
Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
return permissionToRecordAudio

}

private fun hasPermission(result: Result) {
result.success(hasRecordPermission())
}

private fun requestRecordPermission() {
val localActivity = currentActivity
if (!hasRecordPermission() && localActivity != null) {
debugLog("requesting RECORD_AUDIO permission")
ActivityCompat.requestPermissions(localActivity,
arrayOf(Manifest.permission.RECORD_AUDIO), audioRecordPermissionCode)
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?,
grantResults: IntArray?): Boolean {
when (requestCode) {
audioRecordPermissionCode -> {
if (grantResults != null) {
permissionToRecordAudio = grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
}
completeInitializeRecorder()
return true
}
}
return false
}

private fun initializeRecorder(@NonNull call: MethodCall, @NonNull result: Result) {
mRecordSampleRate = call.argument<Int>("sampleRate") ?: mRecordSampleRate
debugLogging = call.argument<Boolean>("showLogs") ?: false
mPeriodFrames = AudioRecord.getMinBufferSize(mRecordSampleRate, AudioFormat.CHANNEL_IN_MONO, mRecordFormat)
mRecorderBufferSize = mPeriodFrames * 2
audioData = ShortArray(mPeriodFrames)
activeResult = result

val localContext = pluginContext
if (null == localContext) {
completeInitializeRecorder()
return
}
permissionToRecordAudio = ContextCompat.checkSelfPermission(localContext,
Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
if (!permissionToRecordAudio) {
requestRecordPermission()
} else {
debugLog("has permission, completing")
completeInitializeRecorder()
}
debugLog("leaving initializeIfPermitted")
}

private fun initRecorder() {
if (mRecorder?.state == AudioRecord.STATE_INITIALIZED) {
return
}
mRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, mRecordSampleRate, AudioFormat.CHANNEL_IN_MONO, mRecordFormat, mRecorderBufferSize)
if (mRecorder != null) {
mListener = createRecordListener()
mRecorder?.positionNotificationPeriod = mPeriodFrames
mRecorder?.setRecordPositionUpdateListener(mListener)
}
}

private fun completeInitializeRecorder() {

debugLog("completeInitialize")
val initResult: HashMap<String, Any> = HashMap()

if (permissionToRecordAudio) {
mRecorder?.release()
initRecorder()
initResult["isMeteringEnabled"] = true
sendRecorderStatus(SoundStreamStatus.Initialized)
}

initResult["success"] = permissionToRecordAudio
debugLog("sending result")
activeResult?.success(initResult)
debugLog("leaving complete")
activeResult = null
}

private fun sendEventMethod(name: String, data: Any) {
val eventData: HashMap<String, Any> = HashMap()
eventData["name"] = name
eventData["data"] = data
methodChannel.invokeMethod("platformEvent", eventData)
}

private fun debugLog(msg: String) {
if (debugLogging) {
Log.d(logTag, msg)
}
}

private fun startRecording(result: Result) {
try {
if (mRecorder!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
result.success(true)
return
}
initRecorder()
mRecorder!!.startRecording()
sendRecorderStatus(SoundStreamStatus.Playing)
result.success(true)
} catch (e: IllegalStateException) {
debugLog("record() failed")
result.error(SoundStreamErrors.FailedToRecord.name, "Failed to start recording", e.localizedMessage)
}
}

private fun stopRecording(result: Result) {
try {
if (mRecorder!!.recordingState == AudioRecord.RECORDSTATE_STOPPED) {
result.success(true)
return
}
mRecorder!!.stop()
sendRecorderStatus(SoundStreamStatus.Stopped)
result.success(true)
} catch (e: IllegalStateException) {
debugLog("record() failed")
result.error(SoundStreamErrors.FailedToRecord.name, "Failed to start recording", e.localizedMessage)
}
}

private fun sendRecorderStatus(status: SoundStreamStatus) {
sendEventMethod("recorderStatus", status.name)
}

private fun createRecordListener(): OnRecordPositionUpdateListener? {
return object : OnRecordPositionUpdateListener {
override fun onMarkerReached(recorder: AudioRecord) {
recorder.read(audioData!!, 0, mRecorderBufferSize)
}

override fun onPeriodicNotification(recorder: AudioRecord) {
val data = audioData!!
val shortOut = recorder.read(data, 0, mPeriodFrames)
// https://flutter.io/platform-channels/#codec
// convert short to int because of platform-channel's limitation
val byteBuffer = ByteBuffer.allocate(shortOut * 2)
byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(data)

sendEventMethod("dataPeriod", byteBuffer.array())
}
}
}
}
70 changes: 70 additions & 0 deletions lib/src/audio/recorder_stream.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:async';
import 'dart:typed_data';
import 'sound_stream.dart';

class RecorderStream {
static final RecorderStream _instance = RecorderStream._internal();
factory RecorderStream() => _instance;

final _audioStreamController = StreamController<Uint8List>.broadcast();

final _recorderStatusController =
StreamController<SoundStreamStatus>.broadcast();

RecorderStream._internal() {
SoundStream();
eventsStreamController.stream.listen(_eventListener);
_recorderStatusController.add(SoundStreamStatus.Unset);
_audioStreamController.add(Uint8List(0));
}

/// Initialize Recorder with specified [sampleRate]
Future<dynamic> initialize({int sampleRate = 16000, bool showLogs = false}) =>
methodChannel.invokeMethod<dynamic>("initializeRecorder", {
"sampleRate": sampleRate,
"showLogs": showLogs,
});

/// Start recording. Recorder will start pushing audio chunks (PCM 16bit data)
/// to audiostream as Uint8List
Future<dynamic> start() =>
methodChannel.invokeMethod<dynamic>("startRecording");

/// Recorder will stop recording and sending audio chunks to the [audioStream].
Future<dynamic> stop() =>
methodChannel.invokeMethod<dynamic>("stopRecording");

/// Current status of the [RecorderStream]
Stream<SoundStreamStatus> get status => _recorderStatusController.stream;

/// Stream of PCM 16bit data from Microphone
Stream<Uint8List> get audioStream => _audioStreamController.stream;

void _eventListener(dynamic event) {
if (event == null) return;
final String eventName = event["name"] ?? "";
switch (eventName) {
case "dataPeriod":
final Uint8List audioData =
Uint8List.fromList(event["data"] ?? []);
if (audioData.isNotEmpty) _audioStreamController.add(audioData);
break;
case "recorderStatus":
final String status = event["data"] ?? "Unset";
_recorderStatusController.add(SoundStreamStatus.values.firstWhere(
(value) => enumToString(value) == status,
orElse: () => SoundStreamStatus.Unset,
));
break;
}
}

/// Stop and close all streams. This cannot be undone
/// Only call this method if you don't want to use this anymore
void dispose() {
stop();
eventsStreamController.close();
_recorderStatusController.close();
_audioStreamController.close();
}
}
Loading

0 comments on commit f5e4d88

Please sign in to comment.