diff --git a/packages/audio_streamer/CHANGELOG.md b/packages/audio_streamer/CHANGELOG.md index 9482014a6..b3ed2a49a 100644 --- a/packages/audio_streamer/CHANGELOG.md +++ b/packages/audio_streamer/CHANGELOG.md @@ -1,3 +1,9 @@ +## 4.0.0 + +* removal of permission_handler dependency - handling permission should take place in the app, not the plugin. +* major refactor of plugin code +* update of example app to handle permissions to access the microphone and other improvements. + ## 3.1.0 * upgrade of permission_handler plugins diff --git a/packages/audio_streamer/LICENSE b/packages/audio_streamer/LICENSE index 7dfb95c95..cefaa069c 100644 --- a/packages/audio_streamer/LICENSE +++ b/packages/audio_streamer/LICENSE @@ -2,16 +2,8 @@ MIT License. Copyright 2019 Copenhagen Center for Health Technology (CACHET) at the Technical University of Denmark (DTU). -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the ”Software”), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ”Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/audio_streamer/README.md b/packages/audio_streamer/README.md index 2c05d11a9..762507ad9 100644 --- a/packages/audio_streamer/README.md +++ b/packages/audio_streamer/README.md @@ -1,21 +1,23 @@ # Audio Streamer -Streaming of PCM audio from Android and iOS with a customizable sampling rate. +Streaming of Pulse-code modulation (PCM) audio from Android and iOS with a customizable sampling rate. ## Permissions +Using this plugin needs permission to access the microphone. Requesting this permission is **NOT** part of the plugin, but should be handled by the app. However, for the app to be able to access the microphone, the app need to have the following permission on Android and iOS. + On **Android** add the audio recording permission to `AndroidManifest.xml`. ```xml ``` -On **iOS** enable the following: +On **iOS** enable the following using XCode: - Capabilities > Background Modes > _Audio, AirPlay and Picture in Picture_ - In the Runner Xcode project edit the `Info.plist` file. Add an entry for _'Privacy - Microphone Usage Description'_ -When editing the `Info.plist` file manually, the entries needed are: +If editing the `Info.plist` file manually, the entries needed are: ```xml NSMicrophoneUsageDescription @@ -26,7 +28,7 @@ When editing the `Info.plist` file manually, the entries needed are: ``` -- Edit the `Podfile` to include the permission for the microphone: +Edit the `Podfile` to include the permission for the microphone: ```ruby post_install do |installer| @@ -43,48 +45,35 @@ post_install do |installer| end ``` -## Example +## Using the plugin -See the file `example/lib/main.dart` for a fully fledged example app using the plugin. -Note that on iOS the sample rate will not necessarily change, as there is only the option to set a preferred one. +The plugin works as a singleton and provide a simple `audioStream` to listen to. ```dart - // Note that AudioStreamer works as a singleton. - AudioStreamer streamer = AudioStreamer(); - bool _isRecording = false; - List _audio = []; - - void onAudio(List buffer) async { - _audio.addAll(buffer); - var sampleRate = await streamer.actualSampleRate; - double secondsRecorded = _audio.length.toDouble() / sampleRate; +AudioStreamer().audioStream.listen( + (List buffer) { print('Max amp: ${buffer.reduce(max)}'); print('Min amp: ${buffer.reduce(min)}'); - print('$secondsRecorded seconds recorded.'); - print('-' * 50); - } - - void handleError(PlatformException error) { + }, + onError: (Object error) { print(error); - } - - void start() async { - try { - // start streaming using default sample rate of 44100 Hz - streamer.start(onAudio, handleError); - - setState(() { - _isRecording = true; - }); - } catch (error) { - print(error); - } - } - - void stop() async { - bool stopped = await streamer.stop(); - setState(() { - _isRecording = stopped; - }); - } + }, + cancelOnError: true, +); ``` + +The sampling rate can be set and read using the `samplingRate` and `actualSampleRate` properties. + +```dart +// Set the sampling rate. Must be done BEFORE listening to the audioStream. +AudioStreamer().sampleRate = 22100; + +// Get the real sampling rate - may be different from the requested sampling rate. +int sampleRate = await AudioStreamer().actualSampleRate; +``` + +## Example + +See the file `example/lib/main.dart` for an example app using the plugin. +This app also illustrates how to ask for permission to access the microphone. +Note that on iOS the sample rate will not necessarily change, as there is only the option to set a preferred one. diff --git a/packages/audio_streamer/analysis_options.yaml b/packages/audio_streamer/analysis_options.yaml new file mode 100644 index 000000000..9e431bbb8 --- /dev/null +++ b/packages/audio_streamer/analysis_options.yaml @@ -0,0 +1,18 @@ +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: [build/**] + language: + strict-casts: true + strict-inference: true + strict-raw-types: false + +linter: + rules: + cancel_subscriptions: true + constant_identifier_names: false + depend_on_referenced_packages: false + avoid_print: false diff --git a/packages/audio_streamer/example/README.md b/packages/audio_streamer/example/README.md index 9ad80d4d5..bdf20a05b 100644 --- a/packages/audio_streamer/example/README.md +++ b/packages/audio_streamer/example/README.md @@ -1,16 +1,3 @@ -# audio_streamer_example +# Audio Streamer Example Demonstrates how to use the audio_streamer plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/packages/audio_streamer/example/lib/main.dart b/packages/audio_streamer/example/lib/main.dart index 1144d793d..ddbaf9d0a 100644 --- a/packages/audio_streamer/example/lib/main.dart +++ b/packages/audio_streamer/example/lib/main.dart @@ -1,93 +1,102 @@ +import 'dart:math'; +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:audio_streamer/audio_streamer.dart'; -import 'dart:math'; +import 'package:permission_handler/permission_handler.dart'; -import 'package:flutter/services.dart'; +void main() => runApp(new AudioStreamingApp()); -void main() { - runApp(new MyApp()); -} - -class MyApp extends StatefulWidget { +class AudioStreamingApp extends StatefulWidget { @override - _MyAppState createState() => new _MyAppState(); + AudioStreamingAppState createState() => new AudioStreamingAppState(); } -class _MyAppState extends State { - // Note that AudioStreamer works as a singleton. - AudioStreamer streamer = AudioStreamer(); +class AudioStreamingAppState extends State { + int? sampleRate; + bool isRecording = false; + List audio = []; + List? latestBuffer; + double? recordingTime; + StreamSubscription>? audioSubscription; - bool _isRecording = false; - List _audio = []; + /// Check if microphone permission is granted. + Future checkPermission() async => await Permission.microphone.isGranted; - @override - void initState() { - super.initState(); - } + /// Request the microphone permission. + Future requestPermission() async => + await Permission.microphone.request(); + /// Call-back on audio sample. void onAudio(List buffer) async { - _audio.addAll(buffer); - var sampleRate = await streamer.actualSampleRate; - double secondsRecorded = _audio.length.toDouble() / sampleRate; - print('Max amp: ${buffer.reduce(max)}'); - print('Min amp: ${buffer.reduce(min)}'); - print('$secondsRecorded seconds recorded.'); - print('-' * 50); + audio.addAll(buffer); + + // Get the actual sampling rate, if not already known. + sampleRate ??= await AudioStreamer().actualSampleRate; + recordingTime = audio.length / sampleRate!; + + setState(() => latestBuffer = buffer); } - void handleError(PlatformException error) { - setState(() { - _isRecording = false; - }); - print(error.message); - print(error.details); + /// Call-back on error. + void handleError(Object error) { + setState(() => isRecording = false); + print(error); } + /// Start audio sampling. void start() async { - try { - // Start streaming using default sample rate of 44100 Hz - streamer.start(onAudio, handleError); - - setState(() { - _isRecording = true; - }); - } catch (error) { - print(error); + // Check permission to use the microphone. + // + // Remember to update the AndroidManifest file (Android) and the + // Info.plist and pod files (iOS). + if (!(await checkPermission())) { + await requestPermission(); } + + // Set the sampling rate - works only on Android. + AudioStreamer().sampleRate = 22100; + + // Start listening to the audio stream. + audioSubscription = + AudioStreamer().audioStream.listen(onAudio, onError: handleError); + + setState(() => isRecording = true); } + /// Stop audio sampling. void stop() async { - bool stopped = await streamer.stop(); - setState(() { - _isRecording = stopped; - }); + audioSubscription?.cancel(); + setState(() => isRecording = false); } - List getContent() => [ - Container( - margin: EdgeInsets.all(25), - child: Column(children: [ - Container( - child: Text(_isRecording ? "Mic: ON" : "Mic: OFF", - style: TextStyle(fontSize: 25, color: Colors.blue)), - margin: EdgeInsets.only(top: 20), - ) - ])), - ]; - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: getContent())), - floatingActionButton: FloatingActionButton( - backgroundColor: _isRecording ? Colors.red : Colors.green, - onPressed: _isRecording ? stop : start, - child: _isRecording ? Icon(Icons.stop) : Icon(Icons.mic)), - ), - ); - } + Widget build(BuildContext context) => MaterialApp( + home: Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.all(25), + child: Column(children: [ + Container( + child: Text(isRecording ? "Mic: ON" : "Mic: OFF", + style: TextStyle(fontSize: 25, color: Colors.blue)), + margin: EdgeInsets.only(top: 20), + ), + Text(''), + Text('Max amp: ${latestBuffer?.reduce(max)}'), + Text('Min amp: ${latestBuffer?.reduce(min)}'), + Text( + '${recordingTime?.toStringAsFixed(2)} seconds recorded.'), + ])), + ])), + floatingActionButton: FloatingActionButton( + backgroundColor: isRecording ? Colors.red : Colors.green, + child: isRecording ? Icon(Icons.stop) : Icon(Icons.mic), + onPressed: isRecording ? stop : start, + ), + ), + ); } diff --git a/packages/audio_streamer/example/pubspec.yaml b/packages/audio_streamer/example/pubspec.yaml index e96b8fb1a..722fb3823 100644 --- a/packages/audio_streamer/example/pubspec.yaml +++ b/packages/audio_streamer/example/pubspec.yaml @@ -3,14 +3,14 @@ description: Demonstrates how to use the audio_streamer plugin. publish_to: 'none' environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + permission_handler: ^11.0.0 cupertino_icons: ^1.0.4 dev_dependencies: @@ -20,44 +20,5 @@ dev_dependencies: audio_streamer: path: ../ -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/audio_streamer/example/test/widget_test.dart b/packages/audio_streamer/example/test/widget_test.dart deleted file mode 100644 index aa2c0cf2b..000000000 --- a/packages/audio_streamer/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:audio_streamer_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/packages/audio_streamer/lib/audio_streamer.dart b/packages/audio_streamer/lib/audio_streamer.dart index 47d38631c..790f0132a 100644 --- a/packages/audio_streamer/lib/audio_streamer.dart +++ b/packages/audio_streamer/lib/audio_streamer.dart @@ -1,21 +1,20 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:permission_handler/permission_handler.dart'; const String EVENT_CHANNEL_NAME = 'audio_streamer.eventChannel'; const String METHOD_CHANNEL_NAME = 'audio_streamer.methodChannel'; -/// API for streaming raw / native audio data. +/// API for streaming raw audio data. class AudioStreamer { - static AudioStreamer? _singleton; - - bool _isRecording = false; static const EventChannel _noiseEventChannel = EventChannel(EVENT_CHANNEL_NAME); static const MethodChannel _sampleRateChannel = MethodChannel(METHOD_CHANNEL_NAME); + static const int DEFAULT_SAMPLING_RATE = 44100; + + int _sampleRate = DEFAULT_SAMPLING_RATE; Stream>? _stream; - StreamSubscription>? _subscription; + static AudioStreamer? _instance; /// Constructs a singleton instance of [AudioStreamer]. /// @@ -23,86 +22,32 @@ class AudioStreamer { // When a second instance is created, the first instance will not be able to listen to the // audio because it is overridden. Forcing the class to be a singleton class can prevent // misuse of creating a second instance from a programmer. - factory AudioStreamer() => _singleton ??= AudioStreamer._(); + factory AudioStreamer() => _instance ??= AudioStreamer._(); AudioStreamer._(); - /// Get the actual sampling rate, may be different from the requested sampling rate - Future get actualSampleRate async => - await _sampleRateChannel.invokeMethod('getSampleRate'); - - /// Verify that microphone permission was granted. - Future checkPermission() async => - await Permission.microphone.request().isGranted; - - /// Request the microphone permission. - Future requestPermission() async => - await Permission.microphone.request(); - - /// Start streaming audio. - /// - /// Only starts if microphone permission was granted - /// - /// The [onData] callback function will be called to handle the audio stream. - /// The [handleError] callback will be called to handle any errors. - /// The [samplingRate] specifies the sampling rate in Hz. + /// The sampling rate in Hz. Must be set before the [audioStream] is used. /// Default sample rate of is 44100 Hz. /// Note that sampling rate can only be set on Android, not on iOS. - Future start( - Function onData, - Function handleError, { - samplingRate = 44100, - }) async { - if (_isRecording) { - print('AudioStreamer: Already recording!'); - return _isRecording; - } else { - bool granted = await checkPermission(); - - if (granted) { - final stream = _makeAudioStream(handleError, samplingRate); - _subscription = stream.listen(onData as void Function(List)?); - _isRecording = true; - } - - // If permission wasn't yet given, then ask for it, and then try recording again. - else { - await requestPermission(); - start(onData, handleError, samplingRate: samplingRate); - } - } - return _isRecording; - } - - /// Stop audio recording. - Future stop() async { - if (_subscription != null) { - _subscription!.cancel(); - _subscription = null; - } - return _isRecording = false; + int get sampleRate => _sampleRate; + set sampleRate(int rate) { + _sampleRate = rate; + _stream = null; } - /// Use EventChannel to receive audio stream from native - Stream> _makeAudioStream( - Function handleErrorFunction, int sampleRate) { - if (_stream == null) { - _stream = _noiseEventChannel - .receiveBroadcastStream({"sampleRate": sampleRate}) - .handleError((error) { - _isRecording = false; - _stream = null; - handleErrorFunction(error); - }) - .map((buffer) => buffer as List?) - .map((list) { - if (list != null && list.isNotEmpty && list[0] is double) - return list.cast(); - return list! - .map((e) => e is double ? e : double.parse('$e')) - .toList(); - }); - } - return _stream!; - } + /// The actual sampling rate. + /// + /// The actual sampling rate may be different from the requested sampling rate. + /// Only available after sampling has started. + Future get actualSampleRate async => + await _sampleRateChannel.invokeMethod('getSampleRate') ?? + DEFAULT_SAMPLING_RATE; + + /// The stream of audio samples. + Stream> get audioStream => _stream ??= _noiseEventChannel + .receiveBroadcastStream({"sampleRate": sampleRate}) + .map((buffer) => buffer as List?) + .map((list) => (list != null && list.isNotEmpty && list[0] is double) + ? list.cast() + : list!.map((e) => e is double ? e : double.parse('$e')).toList()); } diff --git a/packages/audio_streamer/pubspec.yaml b/packages/audio_streamer/pubspec.yaml index f44650c13..e58fcaa7f 100644 --- a/packages/audio_streamer/pubspec.yaml +++ b/packages/audio_streamer/pubspec.yaml @@ -1,6 +1,6 @@ name: audio_streamer description: Streaming of Pulse-code modulation (PCM) audio from Android and iOS -version: 3.1.0 +version: 4.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/ environment: @@ -8,13 +8,13 @@ environment: flutter: ">=3.0.0" dependencies: - permission_handler: ^11.0.0 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter + flutter_lints: any flutter: plugin: