forked from am15h/tflite_flutter_helper
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add TensorAudio and recording support
- Loading branch information
Showing
8 changed files
with
556 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
293 changes: 268 additions & 25 deletions
293
android/src/main/kotlin/com/tfliteflutter/tflite_flutter_helper/TfliteFlutterHelperPlugin.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.