依賴:flutter_sound:^9.2.9
地址:https://pub.flutter-io.cn/packages/flutter_sound/install
音頻:audio_session: ^0.1.6
地址:https://pub.flutter-io.cn/packages/audio_session
依賴:permission_handler: ^8.1.2
地址:https://pub.flutter-io.cn/packages/permission_handler/versions/8.1.2/install
安卓配置
需要配置 AndroidManifest.xml 錄音權(quán)限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
蘋果配置
info.plist
<key>NSMicrophoneUsageDescription</key>
<string>需要您的同意,才能訪問麥克風</string>
podfile
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_MICROPHONE=1']
end
end
end
iOS工程中增加libc++.tbd庫,Build Phases->Link Binary With...
代碼塊:
引入頭文件
import 'package:audio_session/audio_session.dart'; import >'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart';
聲明對象
FlutterSoundRecorder recorderModule = FlutterSoundRecorder(); FlutterSoundPlayer playerModule = FlutterSoundPlayer();
定義初始化方法
void init() async { //開啟錄音 await recorderModule.openRecorder(); //設置訂閱計時器 await recorderModule .setSubscriptionDuration(const Duration(milliseconds: 10)); //設置音頻 final session = await AudioSession.instance; await session.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker, avAudioSessionMode: AVAudioSessionMode.spokenAudio, avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, flags: AndroidAudioFlags.none, usage: AndroidAudioUsage.voiceCommunication, ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidWillPauseWhenDucked: true, )); await playerModule.closePlayer(); await playerModule.openPlayer(); await playerModule .setSubscriptionDuration(const Duration(milliseconds: 10)); }
動態(tài)權(quán)限
Future<bool> getPermissionStatus() async { Permission permission = Permission.microphone; //granted 通過在旱,denied 被拒絕摇零,permanentlyDenied 拒絕且不在提示 PermissionStatus status = await permission.status; if (status.isGranted) { return true; } else if (status.isDenied) { requestPermission(permission); } else if (status.isPermanentlyDenied) { openAppSettings(); } else if (status.isRestricted) { requestPermission(permission); } else {} return false; } ///申請權(quán)限 void requestPermission(Permission permission) async { PermissionStatus status = await permission.request(); if (status.isPermanentlyDenied) { openAppSettings(); } }
錄音、播放
/// 開始錄音 _startRecorder() async { try { var status = await getPermissionStatus(); Directory tempDir = await getTemporaryDirectory(); var time = DateTime.now().millisecondsSinceEpoch; String path = '${tempDir.path}}-$time${ext[Codec.aacADTS.index]}' ; print('===> 準備開始錄音'); await recorderModule.startRecorder( toFile: path, codec: Codec.aacADTS, bitRate: 8000,, sampleRate: 8000,, audioSource: AudioSource.microphone); print('===> 開始錄音'); /// 監(jiān)聽錄音 recorderModule.onProgress!.listen((e) { if (e != null && e.duration != null) { DateTime date = new DateTime.fromMillisecondsSinceEpoch( e.duration.inMilliseconds, isUtc: true); if (date.second >= _maxLength) { print('===> 到達時常停止錄音'); _stopRecorder(); } setState(() { print("時間:${date.second}"); print("當前振幅:${e.decibels}"); num = date.second; setState(() { if (e.decibels! > 0 && e.decibels! < 10) { voiceIco = "Asset/message/icon_diyinliang.png"; } else if (e.decibels! > 20 && e.decibels! < 30) { voiceIco = "Asset/message/icon_zhongyinliang.png"; } else if (e.decibels! > 30 && e.decibels! < 40) { voiceIco = "Asset/message/icon_gaoyinliang.png"; } else if (e.decibels! > 40 && e.decibels! < 50) { voiceIco = "Asset/message/icon_zhongyinliang.png"; } else if (e.decibels! > 50 && e.decibels! < 60) { voiceIco = "Asset/message/icon_diyinliang.png"; } else if (e.decibels! > 70 && e.decibels! < 100) { voiceIco = "Asset/message/icon_gaoyinliang.png"; } else { voiceIco = "Asset/message/icon_diyinliang.png"; } }); }); } }); this.setState(() { _state = RecordPlayState.recording; _path = path; print("path == $path"); }); } catch (err) { setState(() { print(err.toString()); _stopRecorder(); _state = RecordPlayState.record; }); } } /// 結(jié)束錄音 _stopRecorder() async { try { await recorderModule.stopRecorder(); print('stopRecorder===> fliePath:$_path'); widget.stopRecord!(_path, num); } catch (err) { print('stopRecorder error: $err'); } setState(() { _state = RecordPlayState.play; }); } ///開始播放桶蝎,這里做了一個播放狀態(tài)的回調(diào) void startPlayer(path, {Function(dynamic)? callBack}) async { try { if (path.contains('http')) { await playerModule.startPlayer( fromURI: path, codec: Codec.mp3, sampleRate: 44000, whenFinished: () { stopPlayer(); callBack!(0); }); } else { //判斷文件是否存在 if (await _fileExists(path)) { if (playerModule.isPlaying) { playerModule.stopPlayer(); } await playerModule.startPlayer( fromURI: path, codec: Codec.aacADTS, sampleRate: 44000, whenFinished: () { stopPlayer(); callBack!(0); }); } else {} } //監(jiān)聽播放進度 playerModule.onProgress!.listen((e) {}); callBack!(1); } catch (err) { callBack!(0); } } /// 結(jié)束播放 void stopPlayer() async { try { await playerModule.stopPlayer(); } catch (err) {} } ///獲取播放狀態(tài) Future getPlayState() async { return await playerModule.getPlayerState(); } /// 釋放播放器 void releaseFlauto() async { try { await playerModule.closePlayer(); } catch (e) { print(e); } } /// 判斷文件是否存在 Future _fileExists(String path) async { return await File(path).exists(); }
釋放
@override void dispose() { // TODO: implement dispose super.dispose(); recorderModule.closeRecorder(); playerModule.closePlayer(); }
聯(lián)調(diào)真機運行驻仅,安卓錄音播放一氣呵成沒任何問題,但蘋果手機上會發(fā)現(xiàn)無法錄制登渣,在進度監(jiān)聽里返回的date秒數(shù)一直是0噪服,這就很苦惱經(jīng)過資料查找最終找到問題所在并解決
修改機型對應filepath后綴音頻格式
String path =GetPlatform.isAndroid ? '${tempDir.path}}-$time.aac' : '${tempDir.path}}-$time.wav';
修改采樣、比特率
await recorderModule.startRecorder( toFile: path, codec: GetPlatform.isAndroid ? Codec.aacADTS : Codec.pcm16WAV, bitRate: 1411200, sampleRate: 44100, audioSource: AudioSource.microphone); print('===> 開始錄音');
細心的你能發(fā)現(xiàn)bitRate由8000換為1411200胜茧,sampleRate由8000換為44100粘优,這不是瞎換的,這是為了提升音頻的高質(zhì)量,以采樣率為44.1KHz呻顽,以16bit采樣雹顺,聲道數(shù)為2為例,所以sampleRate:44100廊遍,計算出比特率44100162 = 1411200
出現(xiàn)這個問題的根本原因是iOS不兼容.aac,說一句這個插件不支持mp3嬉愧。