การบันทึก Record และเล่นไฟล์เสียง Audio ไฟล์ ใน Flutter

บทความใหม่ ยังไม่ถึงปี โดย Ninenik Narkdee
flutter timer audioplayer record permission

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ flutter timer audioplayer record permission

ดูแล้ว 527 ครั้ง


เนื้อหาตอนต่อไปนี้ เราจะมาดูเกี่ยวกับการจัดการไฟล์เสียง
หรือ 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 มาประยุกต์ใช้งานได้ไม่มาก
ก็น้อย และสามารถนำไปต่อยอดทำส่วนอื่นๆ ได้  เนื้อหาตอนหน้าจะเป็นอะไรรอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 25-09-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_046_25092024_source.rar


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง











เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ