diff --git a/example/pubspec.lock b/example/pubspec.lock index eaecad9f..4a2092f6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -74,6 +74,18 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" matcher: dependency: transitive description: diff --git a/lib/flutter_audio_capture.dart b/lib/flutter_audio_capture.dart index 6afcb049..eecb79b6 100644 --- a/lib/flutter_audio_capture.dart +++ b/lib/flutter_audio_capture.dart @@ -1,27 +1 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; - -const AUDIO_CAPTURE_EVENT_CHANNEL_NAME = "ymd.dev/audio_capture_event_channel"; - -class FlutterAudioCapture { - final EventChannel _audioCaptureEventChannel = - EventChannel(AUDIO_CAPTURE_EVENT_CHANNEL_NAME); - StreamSubscription? _audioCaptureEventChannelSubscription; - - Future start(Function listener, Function onError, - {int sampleRate = 44000, int bufferSize = 5000}) async { - if (_audioCaptureEventChannelSubscription != null) return; - _audioCaptureEventChannelSubscription = _audioCaptureEventChannel - .receiveBroadcastStream({ - "sampleRate": sampleRate, - "bufferSize": bufferSize - }).listen(listener as void Function(dynamic)?, onError: onError); - } - - Future stop() async { - if (_audioCaptureEventChannelSubscription == null) return; - _audioCaptureEventChannelSubscription!.cancel(); - _audioCaptureEventChannelSubscription = null; - } -} +export 'native.dart' if (dart.library.html) 'web.dart'; diff --git a/lib/interface.dart b/lib/interface.dart new file mode 100644 index 00000000..72b73468 --- /dev/null +++ b/lib/interface.dart @@ -0,0 +1,10 @@ +class IAudioCapture { + Future start(Function listener, Function onError, + {int sampleRate = 44000, int bufferSize = 5000}) async { + throw UnimplementedError(); + } + + Future stop() async { + throw UnimplementedError(); + } +} diff --git a/lib/native.dart b/lib/native.dart new file mode 100644 index 00000000..0d637e4c --- /dev/null +++ b/lib/native.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_audio_capture/interface.dart'; + +const AUDIO_CAPTURE_EVENT_CHANNEL_NAME = "ymd.dev/audio_capture_event_channel"; + +class FlutterAudioCapture implements IAudioCapture { + final EventChannel _audioCaptureEventChannel = + EventChannel(AUDIO_CAPTURE_EVENT_CHANNEL_NAME); + StreamSubscription? _audioCaptureEventChannelSubscription; + + @override + Future start(Function listener, Function onError, + {int sampleRate = 44000, int bufferSize = 5000}) async { + if (_audioCaptureEventChannelSubscription != null) return; + _audioCaptureEventChannelSubscription = _audioCaptureEventChannel + .receiveBroadcastStream({ + "sampleRate": sampleRate, + "bufferSize": bufferSize + }).listen(listener as void Function(dynamic)?, onError: onError); + } + + @override + Future stop() async { + if (_audioCaptureEventChannelSubscription == null) return; + _audioCaptureEventChannelSubscription!.cancel(); + _audioCaptureEventChannelSubscription = null; + } +} diff --git a/lib/web.dart b/lib/web.dart new file mode 100644 index 00000000..9017a038 --- /dev/null +++ b/lib/web.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:html'; +import 'dart:web_audio'; +import 'dart:typed_data'; + +import 'package:flutter_audio_capture/interface.dart'; + +class FlutterAudioCapture extends IAudioCapture { + StreamController? controller; + + @override + Future start(Function listener, Function onError, + {int sampleRate = 44000, int bufferSize = 5000}) async { + if (controller == null) { + controller = StreamController(); + controller!.stream.listen((event) { + listener(event); + }, onError: (error) { + onError(); + }); + startRecording(); + } + } + + @override + Future stop() async { + if (controller != null) { + await controller!.close(); + controller = null; + } + } + + Float64List _convertBytesToFloat64(dynamic buf) { + return Float64List.fromList(buf); + } + + startRecording() async { + // build up stuff we need + final stream = await window.navigator.mediaDevices! + .getUserMedia({"audio": true, "video": false}); + final context = AudioContext(); + final source = context.createMediaStreamSource(stream); + final processor = context.createScriptProcessor(4096, 1, 1); + source.connectNode(processor); + processor.connectNode(context.destination!); + final audioStream = processor.onAudioProcess; + var closed = false; + audioStream.listen((e) async { + var sr = e.inputBuffer!.sampleRate; + var buf = e.inputBuffer!.getChannelData(0); + if (controller != null && + !controller!.isClosed && + !controller!.isPaused && + !closed) { + try { + controller!.add(_convertBytesToFloat64(buf)); + } catch (e) {} + } else { + // cancel recording stuff... + if (!closed) { + closed = true; + await context.close(); + } + } + }); + } +} diff --git a/pubspec.lock b/pubspec.lock index b850c9ac..ff26b383 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -151,6 +151,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -158,6 +163,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a6211dc1..9497bdc6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter dev_dependencies: mockito: ^5.0.5 @@ -20,8 +22,8 @@ flutter: plugin: platforms: android: - package: com.ymd.flutter_audio_capture pluginClass: FlutterAudioCapturePlugin + package: com.ymd.flutter_audio_capture ios: pluginClass: FlutterAudioCapturePlugin linux: