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

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
permission record audioplayer timer flutter

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

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


เนื้อหาตอนต่อไปนี้ เราจะมาดูเกี่ยวกับการจัดการไฟล์เสียง
หรือ 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
 
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <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

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

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

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

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 สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


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







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