[flutter/dart] 음성 녹음 & 재생

yevvon·2024년 1월 10일
0

flutter

목록 보기
9/12

먼저 이 기능을 위해 필요한 패키지는

  path_provider: ^2.0.5
  audioplayers: ^5.2.0
  permission_handler: ^11.0.1
  flutter_sound: ^9.2.13

이렇게 4가지가 필요합니다.

import 'package:flutter/material.dart';
import 'package:flutter_sound/flutter_sound.dart' as sound;
import 'package:permission_handler/permission_handler.dart';
import 'package:audioplayers/audioplayers.dart';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

필요한 import입니다.

  Duration duration = Duration.zero; //총 시간
  Duration position = Duration.zero; //진행중인 시간

  //녹음에 필요한 것들
  final recorder = sound.FlutterSoundRecorder();
  bool isRecording = false; //녹음 상태
  String audioPath = ''; //녹음중단 시 경로 받아올 변수
  String playAudioPath = ''; //저장할때 받아올 변수 , 재생 시 필요

  //재생에 필요한 것들
  final AudioPlayer audioPlayer = AudioPlayer(); //오디오 파일을 재생기능 제공
  bool isPlaying = false; //현재 재생중인지

필요한 변수들 입니다.
쓰임은 주석으로 표기하였습니다.

  @override
  void initState() {
    super.initState();

    playAudio();
    //마이크 권한 요청, 녹음 초기화
    initRecorder();

    //재생 상태가 변경될 때마다 상태를 감지하는 이벤트 핸들러
    audioPlayer.onPlayerStateChanged.listen((state) {
      setState(() {
        isPlaying = state == PlayerState.playing;
      });
    });

    //재생 파일의 전체 길이를 감지하는 이벤트 핸들러
    audioPlayer.onDurationChanged.listen((newDuration) {
      setState(() {
        duration = newDuration;
      });
    });

    //재생 중인 파일의 현재 위치를 감지하는 이벤트 핸들러
    audioPlayer.onPositionChanged.listen((newPosition) {
      setState(() {
        position = newPosition;
      });
    });
  }

재생시 슬라이더의 위치를 옮기고 초기화 시키기 위해
initState()에 초기화시켜줍니다.

첫번째로 실행되는 initRecorder() 코드입니다.

  Future initRecorder() async {
    final status = await Permission.microphone.request();

    if (status != PermissionStatus.granted) {
      throw 'Microphone permission not granted';
    }

    await recorder.openRecorder();

    isRecording = true;
    recorder.setSubscriptionDuration(
      const Duration(milliseconds: 500),
    );
  }

@override
  void dispose() {
    recorder.closeRecorder();
    audioPlayer.dispose();
    super.dispose();
  }

녹음 버튼을 새로 눌렀을 때 새로운 내용을 저장하기 위해
dispose()에서 해제시켜줍니다.

녹음

Future record() async {
    if (!isRecording) return;
    await recorder.startRecorder(toFile: 'audio');
  }

녹음 정지

Future<void> stop() async {
    final path = await recorder.stopRecorder(); 
    // 녹음 중지하고, 녹음된 오디오 파일의 경로를 얻음
    audioPath = path!;

    setState(() {
      isRecording = false;
    });

    final savedFilePath = await saveRecordingLocally(); 
    // 녹음된 파일을 로컬에 저장

  }

녹음 시작과 녹음 중지는 패키지에서 제공하는 함수를 통해 작동됩니다.

녹음 중지시 저장해주는 함수를 만들어야합니다.
saveRecordingLocally()를 만들어주어 저장을 하겠습니다.

Future<String> saveRecordingLocally() async {
    if (audioPath.isEmpty) return ''; 
    // 녹음된 오디오 경로가 비어있으면 빈 문자열 반환

    final audioFile = File(audioPath);
    if (!audioFile.existsSync()) return ''; 
    // 파일이 존재하지 않으면 빈 문자열 반환
    
    try {
      final directory = await getApplicationDocumentsDirectory();
      final newPath = p.join(directory.path, 'recordings'); 
      // recordings 디렉터리 생성
      
      final newFile = File(p.join(newPath, 'audio.mp3')); 
      // 여기서 'audio.mp3'는 파일명을 나타냄. 필요에 따라 변경 가능
      
      if (!(await newFile.parent.exists())) {
        await newFile.parent.create(recursive: true); 
        // recordings 디렉터리가 없으면 생성
      }

      await audioFile.copy(newFile.path); // 기존 파일을 새로운 위치로 복사

      playAudioPath = newFile.path;

      return newFile.path; // 새로운 파일의 경로 반환
    } catch (e) {
      print('Error saving recording: $e');
      return ''; // 오류 발생 시 빈 문자열 반환
    }
  }

재생을 할 때 파일 형식으로 불러오기 때문에 파일 형식으로 저장해줍니다.

화면

SliderTheme(
	data: SliderThemeData(
    	inactiveTrackColor: Colors.grey, //재생되지 않은 부분
    ),
    child: Slider(
    	min: 0,
        max: duration.inSeconds.toDouble(),
        value: position.inSeconds.toDouble(),
        onChanged: (value) async {
            setState(() {
            	position = Duration(seconds: value.toInt());
            });
            await audioPlayer.seek(position);
        },
        activeColor: Colors.black, //재생 된 부분
	),
),

재생바를 만들었습니다.

Padding(
    padding: const EdgeInsets.symmetric(horizontal: 16),
    child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
            Text(formatTime(position), style: TextStyle(color: Colors.brown),),
            SizedBox(width: 20),
            CircleAvatar(
            	radius: 15, 
            	backgroundColor: Colors.transparent,
            	child: IconButton(
                	padding:
                	EdgeInsets.only(bottom: 50),
                	icon: Icon(
                    	isPlaying
                    	? Icons.pause
                    	: Icons.play_arrow,
                    	color: Colors.brown,
                	),
                	iconSize: 25,
                	onPressed: () async {
                    	print("isplaying 전 : $isPlaying");
						if (isPlaying) {
                        	//재생중이면
                        	await audioPlayer.pause(); //멈추고
                        	setState(() {
                            	isPlaying = false; //상태변경하기
                        	});
                    	} else {//멈춘 상태였으면
                        	await playAudio();
                        	await audioPlayer.resume(); // 녹음된 오디오 재생
                    	}
                	},
            	),
        	),
        SizedBox(width: 20),
        Text(formatTime(duration), style: TextStyle(color: Colors.brown),),
    	],
	),
)

재생바 아래에 시간 표시와 재생버튼을 만들어주었습니다.

SizedBox(
	child: IconButton(
    	onPressed: () async {
        	if (recorder.isRecording) {
        		await stop();
        	} else {
            	await record();
        	}
        	setState(() {});
    	},
    	icon: Icon(
        	recorder.isRecording ? Icons.stop : Icons.mic,
        	size: 30,
        	color: Colors.black,
    	),
	),
),

녹음 버튼입니다.

최종 만들어진 화면입니다.
안드로이드 폰을 연결했을 때는 파일 재생이 되는 것을 확인 했고
캡스톤 수업에서도 기능 구현 인증까지 받았는데
애뮬레이터에서는 재생이 안되는 것 같은데 아직 잘 모르겠습니다.

전체 코드

import 'package:flutter/material.dart';
import 'package:flutter_sound/flutter_sound.dart' as sound;
import 'package:permission_handler/permission_handler.dart';
import 'package:audioplayers/audioplayers.dart';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

class audio extends StatefulWidget {
  const audio({super.key});

  @override
  State<audio> createState() => _audioState();
}

class _audioState extends State<audio> {
  Duration duration = Duration.zero; //총 시간
  Duration position = Duration.zero; //진행중인 시간

  //녹음에 필요한 것들
  final recorder = sound.FlutterSoundRecorder();
  bool isRecording = false; //녹음 상태
  String audioPath = ''; //녹음중단 시 경로 받아올 변수
  String playAudioPath = ''; //저장할때 받아올 변수 , 재생 시 필요

  //재생에 필요한 것들
  final AudioPlayer audioPlayer = AudioPlayer(); //오디오 파일을 재생하는 기능 제공
  bool isPlaying = false; //현재 재생중인지

  @override
  void initState() {
    super.initState();

    playAudio();
    //마이크 권한 요청, 녹음 초기화
    initRecorder();
    print("datetime now: ${DateTime.now()}");

    //재생 상태가 변경될 때마다 상태를 감지하는 이벤트 핸들러
    audioPlayer.onPlayerStateChanged.listen((state) {
      setState(() {
        isPlaying = state == PlayerState.playing;
      });
      print("헨들러 isplaying : $isPlaying");
    });

    //재생 파일의 전체 길이를 감지하는 이벤트 핸들러
    audioPlayer.onDurationChanged.listen((newDuration) {
      setState(() {
        duration = newDuration;
      });
    });

    //재생 중인 파일의 현재 위치를 감지하는 이벤트 핸들러
    audioPlayer.onPositionChanged.listen((newPosition) {
      setState(() {
        position = newPosition;
      });
      print('Current position: $position');
    });
  }

  @override
  void dispose() {
    recorder.closeRecorder();
    audioPlayer.dispose();
    super.dispose();
  }

  Future<void> playAudio() async {
    try {
      if (isPlaying == PlayerState.playing) {
        await audioPlayer.stop(); // 이미 재생 중인 경우 정지시킵니다.
      }

      await audioPlayer.setSourceDeviceFile(playAudioPath);
      print("duration: $duration");
      await Future.delayed(Duration(seconds: 2));
      print("after wait duration: $duration");

      setState(() {
        duration = duration;
        isPlaying = true;
      });

      audioPlayer.play;

      print('오디오 재생 시작: $playAudioPath');
      print("duration: $duration");
    } catch (e) {
      print("audioPath : $playAudioPath");
      print("오디오 재생 중 오류 발생 : $e");
    }
  }

  Future initRecorder() async {
    final status = await Permission.microphone.request();

    if (status != PermissionStatus.granted) {
      throw 'Microphone permission not granted';
    }

    await recorder.openRecorder();

    isRecording = true;
    recorder.setSubscriptionDuration(
      const Duration(milliseconds: 500),
    );
  }

  //녹음 시작
  Future record() async {
    if (!isRecording) return;
    await recorder.startRecorder(toFile: 'audio');
  }

  //저장함수
  Future<String> saveRecordingLocally() async {
    if (audioPath.isEmpty) return ''; // 녹음된 오디오 경로가 비어있으면 빈 문자열 반환

    final audioFile = File(audioPath);
    if (!audioFile.existsSync()) return ''; // 파일이 존재하지 않으면 빈 문자열 반환
    try {
      final directory = await getApplicationDocumentsDirectory();
      final newPath =
          p.join(directory.path, 'recordings'); // recordings 디렉터리 생성
      final newFile = File(p.join(
          newPath, 'audio.mp3')); // 여기서 'audio.mp3'는 파일명을 나타냅니다. 필요에 따라 변경 가능
      if (!(await newFile.parent.exists())) {
        await newFile.parent.create(recursive: true); // recordings 디렉터리가 없으면 생성
      }

      await audioFile.copy(newFile.path); // 기존 파일을 새로운 위치로 복사

      print('Complete Saving recording: ${newFile.path}');
      playAudioPath = newFile.path;

      return newFile.path; // 새로운 파일의 경로 반환
    } catch (e) {
      print('Error saving recording: $e');
      return ''; // 오류 발생 시 빈 문자열 반환
    }
  }

  // 녹음 중지 & 녹음된 파일의 경로를 가져옴 및 저장
  Future<void> stop() async {
    final path = await recorder.stopRecorder(); // 녹음 중지하고, 녹음된 오디오 파일의 경로를 얻음
    audioPath = path!;

    setState(() {
      isRecording = false;
    });

    final savedFilePath = await saveRecordingLocally(); // 녹음된 파일을 로컬에 저장
    print("savedFilePath: $savedFilePath");
  }

  String formatTime(Duration duration) {
    print("formatTime duration: $duration");

    int minutes = duration.inMinutes.remainder(60);
    int seconds = duration.inSeconds.remainder(60);

    String result = '$minutes:${seconds.toString().padLeft(2, '0')}';

    print("formatTime result: $result");
    return result;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: AppBar(
          centerTitle: true,
          elevation: 0.0,
          title: Text(
            '녹음',
            style: TextStyle(color: Colors.black),
          ),
        ),
        body: Center(
          child: Column(
            children: [
              Container(
                padding: EdgeInsets.fromLTRB(40, 0, 40, 0),
                child: Column(
                  children: [
                    SliderTheme(
                      data: SliderThemeData(
                        inactiveTrackColor: Colors.grey,
                      ),
                      child: Slider(
                        min: 0,
                        max: duration.inSeconds.toDouble(),
                        value: position.inSeconds.toDouble(),
                        onChanged: (value) async {
                          setState(() {
                            position = Duration(seconds: value.toInt());
                          });
                          await audioPlayer.seek(position);
                          //await audioPlayer.resume();
                        },
                        activeColor: Colors.black,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 16),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text(
                            formatTime(position),
                            style: TextStyle(color: Colors.brown),
                          ),
                          SizedBox(width: 20),
                          CircleAvatar(
                            radius: 15,
                            backgroundColor: Colors.transparent,
                            child: IconButton(
                              padding: EdgeInsets.only(bottom: 50),
                              icon: Icon(
                                isPlaying ? Icons.pause : Icons.play_arrow,
                                color: Colors.brown,
                              ),
                              iconSize: 25,
                              onPressed: () async {
                                print("isplaying 전 : $isPlaying");

                                if (isPlaying) {
                                  //재생중이면
                                  await audioPlayer.pause(); //멈춤고
                                  setState(() {
                                    isPlaying = false; //상태변경하기..?
                                  });
                                } else {
                                  //멈춘 상태였으면
                                  await playAudio();
                                  await audioPlayer.resume(); // 녹음된 오디오 재생
                                }
                                print("isplaying 후 : $isPlaying");
                              },
                            ),
                          ),
                          SizedBox(width: 20),
                          Text(
                            formatTime(duration),
                            style: TextStyle(color: Colors.brown),
                          ),
                        ],
                      ),
                    )
                  ],
                ),
              ),
              SizedBox(
                height: 50,
              ),
              SizedBox(
                child: IconButton(
                  onPressed: () async {
                    if (recorder.isRecording) {
                      await stop();
                    } else {
                      await record();
                    }
                    setState(() {});
                  },
                  icon: Icon(
                    recorder.isRecording ? Icons.stop : Icons.mic,
                    size: 30,
                    color: Colors.black,
                  ),
                ),
              ),
            ],
          ),
        ));
  }
}

0개의 댓글

관련 채용 정보