شرح كيفية تسجيل مقطع صوتي في فلاتر | how to record an audio clip in filters
في هذا المقال سوف نشرح لك كيف تقوم بتسجيل مقطع صوتي في flutter وامكانية الاستماع اليه في اي وقت وبعدها تستطيع ارساله الى اي شخص ترغب به كما يحدث في معظم تطبيقات المحادثات ولكن الفرق ان هذا سوف يكون عن طريق زر وليست ضغطه مطوله كما يحدث في باقي التطبيقات والامر بسيط جدا سوف نستخدم مجموعه من المكتبات التي سوف تساعدنا في تنفيذ هذه العملية وجميعها موجوده بالاسفل كما هو موضح لكم تستطيع تثبيت كل هذه المكتبات في تطبيقك حتى تتمكن من استخدامها بدون اي مشاكل .
تعد كيفية تحقيق الأداء الأصلي للتطبيق وكيفية تسهيل تصميم الميزات المتنوعة للأجهزة والأنظمة الأساسية قدر الإمكان على المطورين من الصعوبات الرئيسية التي تواجه أطر العمل عبر الأنظمة الأساسية للجوّال. عند القيام بذلك ، ضع في اعتبارك أن تجربة المستخدم يجب أن تكون متسقة مع دمج العناصر المميزة الخاصة بكل نظام أساسي (Android و iOS).
على الرغم من أن الأطر المشتركة بين الأنظمة الأساسية يمكنها عادةً معالجة المشكلات الخاصة بالمنصة ، إلا أن هناك بعض الوظائف التي لا يمكن إنجازها إلا من خلال التعليمات البرمجية الخاصة بالنظام الأساسي المكتوبة خصيصًا. السؤال الرئيسي هو كيف يمكن لهذه الأطر إنشاء اتصال بين النظام الأساسي والتطبيق المعينين.
Add packages
path_provider: any
permission_handler: any
record: ^3.0.3
simple_ripple_animation: ^0.0.3
just_audio: ^0.9.29
provider: ^6.0.3
How to Add recodr in Project Flutter
هذا الجزء سوف يكون المتحكم الرئيسي في عناصر التطبيق وهو ال cubit او ال state management في تطبيقك حتى تتمكن من تنفيذ الاوامر بدون مشاكل ويتم فيها كتابة الاكواد حتى نفصلها على ال ui وتكون في class منفصل لوحدها كما هو موضح ويمكن الوصول اليه عن طريق استدعاء ال cubit كما سوف نلاحظه .
cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:just_audio/just_audio.dart';
import 'package:record/record.dart';
import 'package:untitled/cubit/main_state.dart';
import 'dart:io';
import 'package:just_audio/just_audio.dart';
import 'package:untitled/pages/recording/services/permission_management.dart';
import 'package:untitled/pages/recording/services/storage_management.dart';
import 'package:untitled/pages/recording/services/toast_services.dart';
abstract class MainState {}
class CubitInitial extends MainState {}
class ChangeRecordState extends MainState {}
class RestRecordState extends MainState {}
class ClearRecordState extends MainState {}
class StartRecordState extends MainState {}
class StopRecordState extends MainState {}
class MainCubit extends Cubit<MainState> {
MainCubit() : super(CubitInitial());
static MainCubit get(context) => BlocProvider.of(context);
final _justAudioPlayer = AudioPlayer();
double _currAudioPlaying = 0.0;
bool _isSongPlaying = false;
String _audioFilePath = '';
bool get isSongPlaying => _isSongPlaying;
String get currSongPath => _audioFilePath;
setAudioFilePath(String filePath) {
_audioFilePath = filePath;
emit(ClearRecordState());
}
clearCurrAudioPath() {
_audioFilePath = '';
emit(ClearRecordState());
}
playAudio(File incomingAudioFile, {bool update = true}) async {
try {
_justAudioPlayer.positionStream.listen((event) {
_currAudioPlaying = event.inMicroseconds.ceilToDouble();
if (update) emit(ClearRecordState());
});
_justAudioPlayer.playerStateStream.listen((event) {
if (event.processingState == ProcessingState.completed) {
_justAudioPlayer.stop();
_reset();
}
});
if (_audioFilePath != incomingAudioFile.path) {
setAudioFilePath(incomingAudioFile.path);
await _justAudioPlayer.setFilePath(incomingAudioFile.path);
updateSongPlayingStatus(true);
await _justAudioPlayer.play();
}
if (_justAudioPlayer.processingState == ProcessingState.idle) {
await _justAudioPlayer.setFilePath(incomingAudioFile.path);
updateSongPlayingStatus(true);
await _justAudioPlayer.play();
} else if (_justAudioPlayer.playing) {
updateSongPlayingStatus(false);
await _justAudioPlayer.pause();
} else if (_justAudioPlayer.processingState == ProcessingState.ready) {
updateSongPlayingStatus(true);
await _justAudioPlayer.play();
} else if (_justAudioPlayer.processingState ==
ProcessingState.completed) {
_reset();
}
} catch (e) {
print('Error in playaudio: $e');
}
}
void _reset() {
_currAudioPlaying = 0.0;
emit(RestRecordState());
updateSongPlayingStatus(false);
}
updateSongPlayingStatus(bool update) {
_isSongPlaying = update;
emit(ChangeRecordState());
}
get currLoadingStatus {
final _currTime = (_currAudioPlaying /
(_justAudioPlayer.duration?.inMicroseconds.ceilToDouble() ?? 1.0));
return _currTime > 1.0 ? 1.0 : _currTime;
}
// ---------------- Record
final Record _record = Record();
bool _isRecording = false;
String _afterRecordingFilePath = '';
bool get isRecording => _isRecording;
String get recordedFilePath => _afterRecordingFilePath;
clearOldData(){
_afterRecordingFilePath = '';
emit(ClearRecordState());
}
recordVoice()async{
final _isPermitted = (await PermissionManagement.recordingPermission()) && (await PermissionManagement.storagePermission());
if(!_isPermitted) return;
if(!(await _record.hasPermission())) return;
final _voiceDirPath = await StorageManagement.getAudioDir;
final _voiceFilePath = StorageManagement.createRecordAudioPath(dirPath: _voiceDirPath, fileName: 'audio_message');
await _record.start(path: _voiceFilePath);
_isRecording = true;
emit(StartRecordState());
showToast('Recording Started');
}
stopRecording()async{
String? _audioFilePath;
if(await _record.isRecording()){
_audioFilePath = await _record.stop();
showToast('Recording Stopped');
}
print('Audio file path: $_audioFilePath');
_isRecording = false;
_afterRecordingFilePath = _audioFilePath ?? '';
emit(StopRecordState());
}
}
التاكد من اعطاء المستخدم صلاحيات وصول التطبيق الى الميكروفون في هاتفك
في هذا الجزء سوف نتاكد من وجود بعض الPermissions في التطبيق حتى لا يحدث مشاكل وهذه الصفحة تسمى بالservices الخاصه بالتطبيق .
services.dart
import 'dart:io';
import 'dart:math';
import 'package:path_provider/path_provider.dart';
class StorageManagement {
static Future<String> makeDirectory({required String dirName}) async {
final Directory? directory = await getExternalStorageDirectory();
final _formattedDirName = '/$dirName/';
final Directory _newDir =
await Directory(directory!.path + _formattedDirName).create();
return _newDir.path;
}
static get getAudioDir async => await makeDirectory(dirName: 'recordings');
static String createRecordAudioPath(
{required String dirPath, required String fileName}) =>
"""$dirPath${fileName.substring(0, min(fileName.length, 100))}_${DateTime.now()}.aac""";
}
class PermissionManagement{
static Future<bool> storagePermission() async{
final status = await Permission.storage.request();
return status == PermissionStatus.granted;
}
static Future<bool> recordingPermission() async{
final status = await Permission.microphone.request();
return status == PermissionStatus.granted;
}
}
showToast(String text,
{bool shortToast = true,
fromBottom = true,
Color color = const Color(0xff4BB543),
Color textColor = Colors.white}) {
Fluttertoast.showToast(
msg: text,
toastLength: shortToast ? Toast.LENGTH_SHORT : Toast.LENGTH_SHORT,
gravity: fromBottom ? ToastGravity.BOTTOM : ToastGravity.TOP,
backgroundColor: color,
textColor: textColor,
fontSize: 16.0);
}
تصميم لعرض المقطع الصوتي الذي تم تسجيله في Flutter
في هذا الجزء والذي يعد اخر جزء عباره عن التصميم الذي سوف نقوم بعرضه عندما ينتهي المستخدم من تسجيل المقطع الصوتي وتشغيله .
ui.dart
class GeeCoders extends StatefulWidget {
const GeeCoders({Key? key}) : super(key: key);
@override
State<GeeCoders> createState() => _GeeCodersState();
}
class _GeeCodersState extends State<GeeCoders> with TickerProviderStateMixin {
late MainCubit cubit;
customizeStatusAndNavigationBar() {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
statusBarColor: Colors.transparent,
systemNavigationBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light));
}
@override
void initState() {
cubit = context.read<MainCubit>();
customizeStatusAndNavigationBar();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MainCubit, MainState>(
builder: (context, state) {
return Scaffold(
backgroundColor: Color(0xE45E3FEC),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
// اذا كان الملف فارغ
cubit.recordedFilePath.isEmpty
? _recordingSection()
: _audioPlayingSection(),
if (cubit.recordedFilePath.isNotEmpty &&
!cubit.isSongPlaying)
const SizedBox(height: 40),
if (cubit.recordedFilePath.isNotEmpty &&
!cubit.isSongPlaying)
_resetButton(),
],
),
),
);
},
);
}
// اظهار تصميم في حالة تسجيل مقطع صوتي او في حالة عدم تسجيل مقطع صوتي
_recordingSection() {
// اذا كان يتم تسجيل المقطع الصوتي فيتم اظهار التصميم مع انميشن
if (cubit.isRecording) {
return InkWell(
onTap: () async => await cubit.stopRecording(),
child: RippleAnimation(
repeat: true,
color: const Color(0xff4BB543),
minRadius: 40,
ripplesCount: 6,
child: _commonIconSection(),
),
);
} else {
// اذا لم يتم تسجيل المقطع الصوتي فيظهر التصميم فقط
return InkWell(
onTap: () async => await cubit.recordVoice(),
child: _commonIconSection(),
);
}
}
// تصميم لايقونة تسجيل المقطع الصوتي
_commonIconSection() {
return Container(
width: 70,
height: 70,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xff4BB543),
borderRadius: BorderRadius.circular(100),
),
child: const Icon(Icons.keyboard_voice_rounded,
color: Colors.white, size: 30),
);
}
// تصميم للمربع الذي يظهر بداخله المقطع الصوتي
_audioPlayingSection() {
return Container(
width: MediaQuery.of(context).size.width - 110,
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 10),
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white,
),
child: Row(
children: [
_audioControllingSection(cubit.recordedFilePath),
_audioProgressSection(),
],
),
);
}
_audioControllingSection(String songPath) {
return IconButton(
onPressed: () async {
if (songPath.isEmpty) return;
await cubit.playAudio(File(songPath));
},
icon: Icon(
cubit.isSongPlaying ? Icons.pause : Icons.play_arrow_rounded),
color: const Color(0xff4BB543),
iconSize: 30,
);
}
_audioProgressSection() {
return Expanded(
child: Container(
width: double.maxFinite,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: LinearPercentIndicator(
percent: cubit.currLoadingStatus,
backgroundColor: Colors.black26,
progressColor: const Color(0xff4BB543),
),
));
}
// حذف المقطع الصوتي
_resetButton() {
return InkWell(
onTap: () => cubit.clearOldData(),
child: Center(
child: Container(
width: 80,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
decoration: BoxDecoration(
color: Colors.redAccent,
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'Reset',
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
),
);
}
}