เนื้อหาตอนต่อไปนี้ เราจะมาดูเกี่ยวกับการจัดการไฟล์เสียง
หรือ audio ไฟล์ใน flutter โดยจะดูทั้งการบันทึกไฟเสียงผ่าน
ไมโครโฟน และการเล่นไฟล์เสียงที่บันทึก เนื้อหานี้เราไม่ได้มาอธิบาย
ทุกๆ ขั้นตอนของการทำงาน เพราะเป็นเนื้อหาที่มีรายละเอียดหรือสิ่งที่
เราควรรู้ค่อนข้างมาก และส่วนใหญ่ก็เป็นการประยุกต์จากบทความที่เคย
อธิบายหรือแนะนำมาแล้ว ไม่ว่าจะเป็นการจัดการ path ไฟล์ต่างๆ ของระบบ
การใช้งานการจัดรูปแบบวันที่และเวลาด้วย intl เหล่านี้เป็นต้น ซึ่งเรามีไฟล์ตัวอย่าง
ให้โหลดในท้ายบทความ
ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง
แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
font_awesome_flutter: ^10.7.0
path_provider: ^2.1.4
record: ^5.1.2
permission_handler: ^11.3.1
audioplayers: ^6.1.0
http: ^1.2.2
intl: ^0.17.0
ใน android เพื่มสิทธิ์การเข้าถึงการบันทึกเสียงและการเขียนไฟล์
ไฟล์ android > app > src > main > AndroidManifest.xml
1 2 3 4 5 6 7 | < uses-permission android:name = "android.permission.INTERNET" /> < uses-permission android:name = "android.permission.RECORD_AUDIO" /> < uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" /> < application .... .... </manifest> |
สิ่งที่เพิ่มเข้ามาคือ คือ record เป็น package สำหรับใช้ในการบันทึกเสียง และแน่นอนว่าเมื่อมีการ
จัดการเกี่ยวกับไฟล์ ก็จะต้องมีส่วนของ path_provider มาเกี่ยวข้อง ต่อมาเป็นส่วนของการขอสิทธิ์
โดยเราใช้ permission_handler ในการกำหนดหรือตรวจสอบการขอสิทธิ์การจัดการ เช่น การใช้
งานไมโครโฟน เป็นต้น และส่วนสุดท้าย ก็จะเป็น audioplayers เป็นส่วนที่เราใช้งานเพื่อใช้แสดงหรือ
เล่นเสียงที่เราบันทึก หรือเสียง audio ไฟล์ที่รองรับในแอป flutter
สำหรับเนื้อหาที่เกี่ยวข้องสามารถทบทวนได้ที่บทความดังนี้
การใช้งาน Path Provider และการเชียนอ่าน File ใน Flutter http://niik.in/1066
การทำ Selected Item ใน ListView เพื่อจัดการ ใน Flutter http://niik.in/1068
จัดการข้อมูลด้วย SQL Database โดยใช้ Sqflite ใน Flutter http://niik.in/1047
ลำดับผลลัพธ์และการทำงานที่เราจะได้เรียนรู้

ในหน้าแรก เราจะมีปุ่มไมโครโฟน เพื่อกดแล้วไปยังหน้าบันทึกเสียง และมีปุ่มเลือกดูไฟล์ ไปยังหน้าดูโครง
สร้างไฟล์ในแอป เพื่อเข้าไปดูไฟล์ที่เราบันทึก ซึ่งจะเก็บไว้ที่โฟลเดอร์ app_flutter เมื่อเราเริ่มบันทึก
ไฟล์ ระบบจะทำการสร้างไฟล์โดยใช้วันที่และเวลาในขณะนั้นเป็นชื่อไฟล์ ในที่นี้บันทีกเป็นไฟล์ wav เรา
สามารถ หยุดชั่วคราวขณะบันทึก และบันทึกต่อในไฟล์เดิมได้ ไฟล์จะถูกสร้างทันทีที่เราเริ่มบันทึก หาก
กดหยุดหรือกดออก ไม่มีผลให้ไฟล์ที่สร้างหายไป
เมื่อเราบันทึกไฟล์แล้ว เราสามารถไปยังหน้าเลือกดูไฟล์ ซึ่งจะถูกเก็บไว้ในโฟลเดอร์ app_flutter
หน้านี้เป็นหน้าที่เราสร้างไว้เพื่อแสดงให้เห็นโครงสร้างการทำงาน สามารถไปประยุกต์หรือสร้างรูปแบบ
ใหม่ได้ เช่น หน้าแสดงรายการที่บันทึก แทนได้ สำหรับหน้านี้เมื่อเราเข้าไปด้านใน สามารถเลือกลบ
ไฟล์ได้ กดค้าง เลือกแล้วลบไฟล์ และเรากำหนดว่า ถ้าเป็นไฟล์นามสกุล wav ถ้ากดจะเปิดหน้าเล่น
ไฟล์เสียงหรือไฟล์ audio และเล่นเสียงที่เราเลือก ทั้งหมดก็จะประมาณนี้
ในเนื้อหาจะเขียนอธิบายไว้ในโค้ด เราจะได้รู้และศึกษาเพิ่มเติมเกี่ยวกับเรื่องของเวลา หรือตัว timer
สำหรับนับเวลาการบันทึก หรือเวลาทำ animation ของปุ่มรวมอยู่ด้วย
ไฟล์ตัวอย่างหน้าแรก home.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | import 'package:flutter/material.dart' ; import 'package:font_awesome_flutter/font_awesome_flutter.dart' ; import 'explorer.dart' ; import 'soundrecord.dart' ; class Home extends StatefulWidget { static const routeName = '/home' ; const Home({Key? key}) : super (key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'Home' ), leading: IconButton( icon: Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ), actions: [ IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => SoundRecord(), ), ); }, // icon: FaIcon(FontAwesomeIcons.microphone), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => Explorer(), ), ); }, // icon: FaIcon(FontAwesomeIcons.folder), ), ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Home Screen' ), ], ) ), ); } } |
ไฟล์หน้านี้ไม่มีอะไร เพียงให้เห็นแค่เราเพิ่มปุ่มสำหรับเชื่อมโยงไปยังหน้าต่างๆ ไว้ตรง action เมนู
ข้อสังเกตในโค้ดคือ จะมีการใช้งาน TickerProviderStateMixin Mixin ในส่วนของ State class
สำหรับให้รองรับเกี่ยวกับการจัดการกับเวลา
การใช้งาน Record แพ็กเก็จเพื่อบันทึกเสียง
คำอธิบายแสดงในโค้ด
ไฟล์ soundrecord.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | import 'dart:async' ; import 'package:flutter/material.dart' ; import 'package:path_provider/path_provider.dart' ; import 'package:permission_handler/permission_handler.dart' ; import 'package:record/record.dart' ; import 'package:intl/intl.dart' ; class SoundRecord extends StatefulWidget { static const routeName = '/soundrecord' ; const SoundRecord({Key? key}) : super (key: key); @override State<StatefulWidget> createState() { return _SoundRecordState(); } } class _SoundRecordState extends State<SoundRecord> with TickerProviderStateMixin { // ส่วนควบคุมจัดการตัวบันทึกเสียง late final AudioRecorder _audioRecorder; String? _audioPath; // path ไฟล์ // สถานะต่างๆ เกี่ยวกับการบันทึกเสียง bool isRecording = false ; bool isPaused = false ; // ตัวควบคุมการจัดการ animation late AnimationController _animationController; Timer? _timer; int _recordedTime = 0; // เวลาในการบันทึก (มิลลิวินาที) @override void initState() { // โหลดตัวควบคุมการบันทึกเสียง _audioRecorder = AudioRecorder(); super .initState(); // ควบคุมการทำ animation ในที่นี้จะเป็นการปรับขนาดของสีพื้นหลัง // รูปไมโครโฟน เพื่อแสดงสถานะว่ากำลังอัดเสียงหรือบันทึกเสียงอยู่ // vsync มักใช้ใน AnimationController เพื่อบอกว่าให้ควบคุมการอัปเดตของ // Animation ตามการรีเฟรชของหน้าจอ เพื่อไม่ให้ Animation เกิดการทำงานเกินความจำเป็น _animationController = AnimationController( vsync: this , duration: const Duration(seconds: 1), lowerBound: 0.7, upperBound: 1.0, )..addStatusListener((status) { if (status == AnimationStatus.completed) { _animationController.reverse(); } else if (status == AnimationStatus.dismissed) { _animationController.forward(); } }); } // ฟังก์ชั่นสำหรับบันทึกเสียง Future<void> _startRecording() async { // ดึงเวลาปัจจุบัน DateTime now = DateTime.now(); // จัดรูปแบบวันที่และเวลา String formattedDate = DateFormat( "dd-MM-yyyy'T'HHmmss" ).format(now); // สร้างชื่อไฟล์ String fileName = 'file_$formattedDate.wav' ; try { print( "debug: RECORDING" ); // เริ่มการบันทึกไฟล์ไว้ที่โฟลเดอร์ app_flutter String filePath = await getApplicationDocumentsDirectory() .then((value) => '${value.path}/${fileName}' ); // จัดการสถาะการทำงานต่างๆ setState(() { isRecording = true ; isPaused = false ; _recordedTime = 0; }); // ตัว animation เริ่มทำงาน _animationController.forward(); // ตัวนับเวลาเริ่มทำงาน ให้ทำงานทุกๆ 1 วินาที เพื่อบวกเวลาที่กำลังบันทึก _timer = Timer.periodic(Duration(seconds: 1), (timer) { setState(() { _recordedTime++; }); }); // เริ่มการบันทึกไฟล์เสียง ในที่นี้เราใช้เป็นไฟล์ wav await _audioRecorder.start( const RecordConfig( // specify the codec to be `.wav` encoder: AudioEncoder.wav, ), path: filePath, ); } catch (e) { print( "debug: ERROR WHILE RECORDING: $e" ); } } // ฟังก์ชั่นหยุดชั่วคราว การบันทึกเสียง void _pauseRecording() { // ถ้าหยุดอยู่หรือไม่ หยุอยู่ ให้เิริ่มบันทึกต่อ // ตัวอย่างนี้ ในโค้ดนี้จะไม่ทำงาน จะทำงานตรงหยุดอย่างเดียว // แต่คงไว้หรือเราต้องการใช้ปุ่ม เล่นกับปุ่มหยุดเป็นปุ่มเดียวกัน สามารถใช้งานได้ if (isPaused) { // Resume recording setState(() { isPaused = false ; }); // เริ่มทำการบันทึกต่อ _audioRecorder.resume(); // ตัวควบคุม animation ทำงานต่อ _animationController.forward(); // เริ่มนับการจับเวลาที่บันทึกต่อ _timer = Timer.periodic(Duration(seconds: 1), (timer) { setState(() { _recordedTime++; }); }); } else { // หยุดการบันทึกชั่วคราว setState(() { isPaused = true ; }); // หยุดตัวบันทึกเสียง _audioRecorder.pause(); // หยุดตัวควบคุม animation _animationController.stop(); // ตัวเวลาให้ยกเลิกการทำงาน _timer?.cancel(); } } // ฟังก์ชั่นสำหรับหยุดการบันทึกเสียง Future<void> _stopRecording() async { try { // เมือทำการหยุดการบันทึกเสียง จะส่งค่าตำแหน่งไฟล์กลับมา String? path = await _audioRecorder.stop(); // กำหนดค่าต่างๆ setState(() { _audioPath = path!; isRecording = false ; isPaused = false ; }); // หยุดตัวควบคุมการทำ animation และ รีเซ็ตค่า _animationController.stop(); _animationController.reset(); // ตัวเวลาให้ยกเลิกการทำงาน _timer?.cancel(); print( "debug: PATH: $_audioPath " ); } catch (e) { print( "debug: ERROR WHILE STOP RECORDING: $e" ); } } // ฟังก์ชั่นสำหรับเรียกใช้งานการ เรียกฟังก์ชั่นบันทึกเสียง // ส่วนนี้จะครอบคลุมถึงการขอสิทธิ์การใช้งานต่างๆ void _record() async { // ยังไม่ได้เริ่มบันทึกเสียง if (isRecording == false ) { // ขอดูสิทธิ์การใช้ไมโครโฟน final status = await Permission.microphone.request(); // หากได้รับสิทธิ์การใช้งาน if (status == PermissionStatus.granted) { // เปลี่ยนสถานะการบันทึกเสียง setState(() { isRecording = true ; }); // เรียกใช้งานฟังก์ชั่นการบันทึกเสียง await _startRecording(); } else if (status == PermissionStatus.permanentlyDenied) { print( "debug: Permission permanently denied" ); } } else { // ถ้าบันทึกเสียงอยู่ หากเรียกใช้จะเป็นการ หยุดบันทึกเสียงแทน await _stopRecording(); setState(() { isRecording = false ; }); } } @override void dispose() { _audioRecorder.dispose(); _animationController.dispose(); _timer?.cancel(); super .dispose(); } // ฟังก์ชันแปลงเวลาเป็นรูปแบบ mm:ss String _formatTime(int seconds) { int minutes = (seconds ~/ 60) % 60; int _seconds = seconds % 60; return '${minutes.toString().padLeft(2, ' 0 ')}:' '${_seconds.toString().padLeft(2, ' 0 ')}' ; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'บันทึกเสียง' ), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Center( child: isRecording ? ScaleTransition( scale: _animationController, child: Container( width: 150, height: 150, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.red.withOpacity(0.7), ), child: Icon( Icons.mic, size: 80, color: Colors.white, ), ), ) : Container( width: 150, height: 150, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey, ), child: Icon( Icons.mic_off, size: 80, color: Colors.white, ), ), ), SizedBox(height: 20), // แสดงเวลาในการบันทึก Text( _formatTime(_recordedTime), style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, ), ), SizedBox(height: 40), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Start/Pause Button ElevatedButton.icon( onPressed: isRecording ? _pauseRecording : _record, icon: Icon(isRecording ? (isPaused ? Icons.play_arrow : Icons.pause) : Icons.mic), label: Text( isRecording ? (isPaused ? 'Resume' : 'Pause' ) : 'Start' ), ), SizedBox(width: 20), // Stop Button if (isRecording) ElevatedButton.icon( onPressed: _stopRecording, icon: Icon(Icons.stop), label: Text( 'Stop' ), ), ], ), ], ), ); } } |
ผลลัพธ์การทำงาน

หน้านี้เราใช้สำหรับบันทึกเสียง สามารถปรับแต่งเพิ่มเติมได้ตามต้องการ เช่น การแจ้งเมื่อบันทึกเสียง
เรียบร้อยแล้ว หรืออื่นๆ ตามต้องการ
การใช้งาน Audioplayers แพ็กเก็จเพื่อเล่นไฟล์เสียง
แพ็กเก็จตัวนี้สามารถเล่นไฟล์เสียงได้หลายชนิด สำหรับหน้า audioplayer เราจะทำการส่ง path
ไฟล์เสียงแบบเต็มเข้ามายังหน้านี้แล้วให้เริ่มไฟล์ทันที คำอธิบายแสดงในโค้ด
ไฟล์ audioplayer.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | import 'dart:async' ; import 'package:flutter/material.dart' ; import 'package:audioplayers/audioplayers.dart' ; class AudioPlayers extends StatefulWidget { static const routeName = '/audioplayer' ; final String fileName; // เพิ่มพารามิเตอร์ fileName const AudioPlayers({ Key? key, required this .fileName} ) : super (key: key); @override State<StatefulWidget> createState() { return _AudioPlayersState(); } } class _AudioPlayersState extends State<AudioPlayers> { // กำหนดตัวแปรสำรับตัวเล่นไฟล์เสียง late AudioPlayer player = AudioPlayer(); @override void initState() { super .initState(); // กำหนดค่า player ให้กับตัวแปร. player = AudioPlayer(); // กำหนดสถานะการทำงานของไฟล์เสียง ถ้าสิ้นสุดการเล่นไฟล์แล้ว player.setReleaseMode(ReleaseMode.stop); // เล่นเสียงทันทีเมื่อหน้าหน้าแสดง WidgetsBinding.instance.addPostFrameCallback((_) async { // เล่นไฟล์ที่รับจากตำแหน่งไฟล์ที่ส่งมา await player.setSource(DeviceFileSource(widget.fileName)); // เริ่มเล่นไฟล์ await player.resume(); }); } @override void dispose() { player.dispose(); super .dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'AudioPlayer' ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'AudioPlayer Screen' ), PlayerWidget(player: player), ], )), ); } } // คลาสส่วนของการกำหนดหน้าตาของ player หรือหน้าเล่นไฟล์เสียง class PlayerWidget extends StatefulWidget { final AudioPlayer player; const PlayerWidget({ required this .player, super .key, }); @override State<StatefulWidget> createState() { return _PlayerWidgetState(); } } class _PlayerWidgetState extends State<PlayerWidget> { // ตัวกำหนดและจัดการเกี่ยวกับการเล่นเสียง ไม่ว่าจะเป็นตำแหน่งเสียงที่กำลังเล่น // ว่าเล่นอยู่ที่เวลาเท่าไหร่ สถานะการเล่นไฟล์เสียง เป็นตัน PlayerState? _playerState; Duration? _duration; Duration? _position; // สถานะต่างๆ ของการควบคุม steram ไฟล์เสียง StreamSubscription? _durationSubscription; StreamSubscription? _positionSubscription; StreamSubscription? _playerCompleteSubscription; StreamSubscription? _playerStateChangeSubscription; bool get _isPlaying => _playerState == PlayerState.playing; bool get _isPaused => _playerState == PlayerState.paused; String get _durationText => _duration?.toString().split( '.' ).first ?? '' ; String get _positionText => _position?.toString().split( '.' ).first ?? '' ; AudioPlayer get player => widget.player; @override void initState() { super .initState(); // ใช้ค่าเริ่มต้นสถานะจาก player _playerState = player.state; // ดึงข้อมูลระยะเวลาของไฟล์เสียงที่จะเล่น player.getDuration().then( (value) => setState(() { _duration = value; }), ); // ดังข้อมูลตำแหน่งไฟล์เสียงที่กำลังเล่น player.getCurrentPosition().then( (value) => setState(() { _position = value; }), ); // เรียกใช้ฟังก์ชั่นตรวจจับการเปลี่ยนแปลงของไฟล์ stream เสียง _initStreams(); } // เป็นฟังก์ชันที่ใช้ในการเปลี่ยนแปลงสถานะภายในวิดเจ็ต @override void setState(VoidCallback fn) { // ใช้ตรวจสอบว่าวิดเจ็ตยังคงถูก "mount" อยู่หรือไม่ // (หมายถึงวิดเจ็ตยังคงอยู่ในหน้าจอหรือไม่) // เพื่อป้องกันการเกิดข้อผิดพลาดที่เรียกว่า "setState() called after dispose" // ซึ่งจะเกิดเมื่อเราเรียก setState() บนวิดเจ็ตที่ถูกลบไปแล้ว if (mounted) { super .setState(fn); } } @override void dispose() { _durationSubscription?.cancel(); _positionSubscription?.cancel(); _playerCompleteSubscription?.cancel(); _playerStateChangeSubscription?.cancel(); super .dispose(); } @override Widget build(BuildContext context) { final color = Theme.of(context).primaryColor; return Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( key: const Key( 'play_button' ), onPressed: _isPlaying ? null : _play, iconSize: 48.0, icon: const Icon(Icons.play_arrow), color: color, ), IconButton( key: const Key( 'pause_button' ), onPressed: _isPlaying ? _pause : null , iconSize: 48.0, icon: const Icon(Icons.pause), color: color, ), IconButton( key: const Key( 'stop_button' ), onPressed: _isPlaying || _isPaused ? _stop : null , iconSize: 48.0, icon: const Icon(Icons.stop), color: color, ), ], ), Slider( onChanged: (value) { // ส่วนของการเลื่อนตำแหน่งการเล่นเสียง final duration = _duration; if (duration == null ) { return ; } final position = value * duration.inMilliseconds; player.seek(Duration(milliseconds: position.round())); }, value: (_position != null && _duration != null && _position!.inMilliseconds > 0 && _position!.inMilliseconds < _duration!.inMilliseconds) ? _position!.inMilliseconds / _duration!.inMilliseconds : 0.0, ), Text( _position != null ? '$_positionText / $_durationText' : _duration != null ? _durationText : '' , style: const TextStyle(fontSize: 16.0), ), ], ); } // ฟังก์ชั่นตรวจนับค่าต่างๆ ขณะมีการเล่นเสียงหรือมีการเปลี่ยนแปลงข้อมูลเสียง void _initStreams() { // ขณะเวลาเสียงเล่นที่เปลี่ยนแปลง _durationSubscription = player.onDurationChanged.listen((duration) { setState(() => _duration = duration); }); // ขณะเวลาตำแหน่งเสียงที่เล่นมีการเปลี่ยนแปลง _positionSubscription = player.onPositionChanged.listen( (p) => setState(() => _position = p), ); // เมื่อสิ้นสุดการเล่นเสียง หรือเล่นเสียงจบแล้ว _playerCompleteSubscription = player.onPlayerComplete.listen((event) { setState(() { _playerState = PlayerState.stopped; _position = Duration.zero; }); }); // สถานะของเครื่องเล่นเสียง _playerStateChangeSubscription = player.onPlayerStateChanged.listen((state) { setState(() { _playerState = state; }); }); } // ฟังก์ชั่นเล่นเสียง Future<void> _play() async { await player.resume(); setState(() => _playerState = PlayerState.playing); } // ฟังก์ชั่นหยุดการเล่นเสียงชั่วคราว Future<void> _pause() async { await player.pause(); setState(() => _playerState = PlayerState.paused); } // ฟังก์ชั่นหยุดการเล่นเสียง Future<void> _stop() async { await player.stop(); setState(() { _playerState = PlayerState.stopped; _position = Duration.zero; }); } } |
ผลลัพธ์การทำงานที่ได้

เมื่อเปิดมายังหน้านี้โดยส่ง path ของไฟล์เสียงมาเพื่อนำมาเล่น ทันทีที่หน้านี้แสดงขึ้นมาก็จะเริ่มเล่นไฟล์
เสียงที่ส่งเข้ามา เราสามารถหยุดชั่วคราว หรือหยุด หรือเล่นไปยังตำแหน่งต่างๆ ได้
หน้าแสดงรายการเสียงที่บันทึก
หน้านี้เราใช้จากโค้ดในบทความเก่า ซึ่งสามารถไปประยุกต์ในรูปแบบอื่นได้ตามต้องการ ในที่นี้จะขอ
นำมาอธิบายเล็กน้อยในส่วนของการส่ง path ไฟล์ไปแสดงในหน้า audioplayer โดยเราจะตรวจว่า
ไฟล์นั้นนามสกุล .wav หรือไม่ ถ้าใช้ก็ให้สามารถกดแล้วเปิดหน้าเล่นเสียงได้
ดูเฉพาะส่วนนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // กรณีอยากให้รองรับไฟล์เสียงนามสกุลต่างๆ สามารถใช้แบบนี้แทนได้ // List<String> audioExtensions = ['.mp3', '.wav', '.flac', '.aac', '.ogg']; // if (audioExtensions.any((ext) => fileName.toLowerCase().endsWith(ext))) { if (fileName.toLowerCase().endsWith( '.wav' )) { print( 'debug: The file is a .wav file.' ); print( "debug: path ${_currentPath}" ); String fullPathFile = '$_currentPath/$fileName' ; Navigator.push( context, MaterialPageRoute( builder: (context) => AudioPlayers(fileName: fullPathFile,), ), ); } else { print( 'debug: The file is not a .wav file.' ); } |
ไฟล์ explorer.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | import 'dart:io' ; import 'package:flutter/material.dart' ; import 'package:font_awesome_flutter/font_awesome_flutter.dart' ; import 'package:path_provider/path_provider.dart' ; import 'audioplayer.dart' ; class Explorer extends StatefulWidget { static const routeName = '/explorer' ; const Explorer({Key? key}) : super (key: key); @override State<StatefulWidget> createState() { return _ExplorerState(); } } class _ExplorerState extends State<Explorer> { List<FileSystemEntity?>? _folders; String _currentPath = '' ; // เก็บ path ปัจจุบัน Directory? _currentFolder; // เก็บ โฟลเดอร์ที่กำลังใช้งาน // ตัวแปรเก็บ index รายการที่เลือก List<int> _selectedItems = []; @override void initState() { // TODO: implement initState super .initState(); _loadFolder(); } void _loadFolder() async { // ข้อมูลเกี่ยวกับโฟลเดอร์ Directory ต่างๆ // final tempDirectory = await getTemporaryDirectory(); // final appSupportDirectory = await getApplicationSupportDirectory(); final appDocumentsDirectory = await getApplicationDocumentsDirectory(); // final externalDocumentsDirectory = await getExternalStorageDirectory(); // final externalStorageDirectories = // await getExternalStorageDirectories(type: StorageDirectory.music); // final externalCacheDirectories = await getExternalCacheDirectories(); /* print(tempDirectory); print(appSupportDirectory); print(appDocumentsDirectory); print(externalDocumentsDirectory); print(externalCacheDirectories); print(externalStorageDirectories); */ // เมื่อโหลดขึ้นมา เาจะเปิดโฟลเดอร์ของ package เป้นโฟลเดอร์หลัก _currentFolder = appDocumentsDirectory.parent; _currentPath = appDocumentsDirectory.parent.path; final myDir = Directory(_currentPath); setState(() { _folders = myDir.listSync(recursive: false , followLinks: false ); }); } // เปิดโฟลเดอร์ และแสดงรายการในโฟลเดอร์ void _setPath(dir) async { _currentFolder = dir; _currentPath = dir.path; final myDir = Directory(_currentPath); try { setState(() { _folders = myDir.listSync(recursive: false , followLinks: false ); }); } catch (e) { print(e); } _selectedItems.clear(); // ล้างค่าการเลือกทั้งหมด } // คำสังลบไฟล์ void _deleteFile(path) async { final deletefile = File(path); // กำหนด file object final isExits = await deletefile.exists(); // เช็คว่ามีไฟล์หรือไม่ if (isExits) { // ถ้ามีไฟล์ try { await deletefile. delete (); } catch (e) { print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // คำสั่งลบโฟลเดอร์ void _deleteFolder(path) async { final deleteFolder = Directory(path); // สร้าง directory object var isExits = await deleteFolder.exists(); // เช็คว่ามีแล้วหรือไม่ if (isExits) { // ถ้ามีโฟลเดอร์ try { await deleteFolder. delete (recursive: true ); } catch (e) { print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // ลบข้อมูลที่เลือกทั้งหมด void _deleteAll() async { bool _confirm; // สร้างตัวแปรรับค่า ยืนยันการลบ _confirm = await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text( "Confirm" ), content: const Text( "Are you sure you wish to delete selected item?" ), actions: <Widget>[ ElevatedButton( onPressed: () => Navigator.of(context).pop( true ), child: const Text( "DELETE" )), ElevatedButton( onPressed: () => Navigator.of(context).pop( false ), child: const Text( "CANCEL" ), ), ], ); }, ); if (_confirm) { // ถ้ายืนยันการลบ เป็น true try { // วนลูป index แล้วอ้างอึงข้อมูลไฟล์ จากนั้นใช้คำสั่ง delete() แบบรองรับการลบข้อมูลด้านในถ้ามี // ในกรณีเป็นโฟลเดอร์ _selectedItems.forEach((index) async { await _folders![index]!. delete (recursive: true ); }); } catch (e) { print(e); } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } } // จำลองสร้างไฟล์ใหม่ void _newFile() async { String newFile = "${_currentFolder!.path}/myfile.txt" ; final myfile = File(newFile); // กำหนด file object final isExits = await myfile.exists(); // เช็คว่ามีไฟล์หรือไม่ if (!isExits) { // ถ้ายังไม่มีไฟล์ try { // สร้างไฟล์ text var file = await myfile.writeAsString( 'Hello World' ); print(file); } catch (e) { print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } // คำสั่งจำลองการสร้างโฟลเดอร์ void _newFolder() async { String newFolder = "${_currentFolder!.path}/mydir" ; final myDir = Directory(newFolder); // สร้าง directory object var isExits = await myDir.exists(); // เช็คว่ามีแล้วหรือไม่ if (!isExits) { // ถ้ายังไม่มีสร้างโฟลเดอร์ขึ้นมาใหม่ try { var directory = await Directory(newFolder).create(recursive: true ); print(directory); } catch (e) { print(e); } } // โหลดข้อมูลใหม่อีกครั้ง setState(() { _setPath(_currentFolder!); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'Explorer' ), actions: <Widget>[ // IconButton( onPressed: _newFolder, // สร้างโฟลเดอร์ใหม่ icon: FaIcon(FontAwesomeIcons.folderPlus), ), IconButton( onPressed: _newFile, // สร้างไฟล์ใหม่ icon: FaIcon(FontAwesomeIcons.fileAlt), ), if (_selectedItems.isNotEmpty) IconButton( onPressed: _deleteAll, icon: FaIcon(FontAwesomeIcons.trashAlt), ), ], ), body: Column( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ ListTile( leading: FaIcon(FontAwesomeIcons.angleLeft), title: Text( '${_currentPath.replaceAll(' /data/user/0/com.example.demo_app ', ' / ')}' ), onTap: () { _setPath(_currentFolder!.parent); }), Expanded( child: _folders != null // เมื่อไม่ใช่ค่า null ? ListView.separated( // กรณีมีรายการ แสดงปกติ itemCount: _folders == null ? 0 : _folders!.length, itemBuilder: (context, index) { var isFolder = _folders![index] is Directory ? true : false ; // เช็คว่าเป็นโฟลเดอร์ var isFile = _folders![index] is File ? true : false ; // เช็คว่าเป็นไฟล์ if (_folders![index] != null ) { // เอาเฉพาะชื่อหลัง / ตัวสุดท้าย String fileName = _folders![index]!.path.split( '/' ).last; return Dismissible( direction: DismissDirection.horizontal, key: UniqueKey(), // dismissThresholds: const { DismissDirection.endToStart:1.0, DismissDirection.startToEnd:1.0}, confirmDismiss: (direction) async { return await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text( "Confirm" ), content: const Text( "Are you sure you wish to delete this item?" ), actions: <Widget>[ ElevatedButton( onPressed: () => Navigator.of(context).pop( true ), child: const Text( "DELETE" )), ElevatedButton( onPressed: () => Navigator.of(context).pop( false ), child: const Text( "CANCEL" ), ), ], ); }, ); }, onDismissed: (direction) { // ปัดไปทางขวา - บนลงล่าง if (direction == DismissDirection.startToEnd) {} // ปัดไปซ้าย - ล่างขึ้นบน if (direction == DismissDirection.endToStart) { try { setState(() { if (isFile) { // ถ้าเป็นไฟล์ ส่ง path ไฟล์ไปลบ _deleteFile(_folders![index]!.path); } if (isFolder) { // ถ้าเป็นโฟลเดอร์ส่ง path โฟลเดอร์ไปลบ _deleteFolder(_folders![index]!.path); } // ต้องลบข้อมูลก่อน แล้วค่อยลบรายการในลิส _folders!.removeAt(index); }); } catch (e) { print(e); } } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text( '$index dismissed' ))); }, background: Container( color: Colors.green, ), secondaryBackground: Container( color: Colors.red, child: Align( alignment: Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric( horizontal: 10.0), child: FaIcon(FontAwesomeIcons.trashAlt), ))), child: ListTile( selected: _selectedItems.contains(index) ? true : false , leading: isFolder ? FaIcon(FontAwesomeIcons.solidFolder) : FaIcon(FontAwesomeIcons.file), trailing: Visibility( visible: _selectedItems.contains(index) ? true : false , child: FaIcon(FontAwesomeIcons.checkCircle), ), title: Text( '${fileName}' ), onLongPress: () { if (!_selectedItems.contains(index)) { setState(() { _selectedItems.add(index); }); } }, onTap: (isFolder == true ) ? () { // กรณีเป้นโฟลเดอร์ if (_selectedItems.contains(index)) { setState(() { _selectedItems.removeWhere( (val) => val == index); }); } else { if (_selectedItems.isNotEmpty) { setState(() { _selectedItems.add(index); }); } else { _setPath(_folders![ index]!); // ถ้ากด ให้ทำคำสั่งเปิดโฟลเดอร์ } } } : () { if (_selectedItems.contains(index)) { setState(() { _selectedItems.removeWhere( (val) => val == index); }); } else { if (_selectedItems.isNotEmpty) { setState(() { _selectedItems.add(index); }); } else { if (fileName.toLowerCase().endsWith( '.wav' )) { print( 'debug: The file is a .wav file.' ); print( "debug: path ${_currentPath}" ); String fullPathFile = '$_currentPath/$fileName' ; Navigator.push( context, MaterialPageRoute( builder: (context) => AudioPlayers(fileName: fullPathFile,), ), ); } else { print( 'debug: The file is not a .wav file.' ); } } } }, // กรณีเป็นไฟล์ )); } else { return Container(); } }, separatorBuilder: (BuildContext context, int index) => const Divider( height: 1, ), ) : const Center(child: Text( 'No items' )), // กรณีไม่มีรายการ ), ], ), ); } } |
ผลลัพธ์และการทำงาน

หน้าตัวอย่างโครงสร้างไฟล์นี้เราจะเห็นโครงสร้างภายในไฟล์ โดยไฟล์ที่ผู้ใช้สามารถเข้าถึงหรือแชร์ได้มัก
จะเก็บไว้ในโฟลเดอร์ app_flutter ถ้าไฟล์สำคัญเช่นไฟล์ฐานข้อมูล ไฟล์การตั้งค่าต่างๆ มักจะเก็บ
ไว้ในโฟลเดอร์ files ส่วนไฟล์แคชหรือไฟล์ชั่วคราวจะอยู่ใน cache
หวังว่าเนื้อหานี้จะทำให้เราได้รู้จักการใช้งานความสามารถของ flutter มาประยุกต์ใช้งานได้ไม่มาก
ก็น้อย และสามารถนำไปต่อยอดทำส่วนอื่นๆ ได้ เนื้อหาตอนหน้าจะเป็นอะไรรอติดตาม