Swift 14주차 - 음악 재생하기

2024. 6. 14. 20:59swift

Swift 14주차 - 음악 재생하기

 

이번 주차에서는 AVAudioPlayer를 이용하여 오디오 파일을 재생, 일시 정지 및 정지하는 방법과 볼륨을 조절하는 방법을 알아볼 예정이다.

 

목차

     

     

    기본 환경 구성하기

     

    먼저 라이브러리 버튼을 클릭하고 레이블을 찾아 스토리보드에 추가하고 'Audio Player'로 수정한 후 글씨 크기(Font)를 'System 24.0'으로 수정하자.

     

     

    이번엔 프로그레스 뷰(Progress View)를 찾아 스토리보드의 레이블(Audio Player) 아래에 배치하자.

     

     

    앞에서와 같은 방법으로 프로그레스 뷰(Progress View) 아래쪽에 레이블 두개를 추가하자.

     

    그리고 내용을 'currentTime'과 'endTime'으로 수정하자.

    이때 endTime은 오른쪽 정렬하자.

     

     

    아래 사진과 같이 currentTime과 endTime 아래에 버튼 3개를 배치한 후 내용을 'Play', 'Pause', 'Stop'으로 변경하자.

     

    버튼 추가 시 화면 오른쪽 인스펙터 영역에서 스타일(Style) 항목을 'Default'로 변경하자.

     

    그리고 그 버튼들 아래에는 레이블을 왼쪽에 놓고 'Volume'으로 수정하자.

     

     

    슬라이더(Slider)를 레이블(Volume)의 오른쪽에 배치하자.

     

     

     

    두 개의 레이블(currentTime, EndTime)을 도큐먼트 아웃라인에서 Command 버튼을 누른 채 선택한 후 'Embed in -> Stack View'를 선택하자.

     

    이런 방법으로 두 개의 레이블을 스택 뷰로 묶을 수 있다.

     

     

     

    같은 방법으로 Play, Pause, Stop 버튼들을 선택한 후 'Embed in -> Stack View'를 선택하여 스택 뷰로 묶고 가운데로 오도록 키보드 방향키를 이용하여 이동하자.

     

     

     

    같은 방법으로 레이블과 슬라이터를 선택한 후 'Embed in -> Stack View'를 선택하여 스택 뷰로 묶자.

     

     

     

    위에서는 가로로 배치된 컴포넌트를 '가로 스택 뷰'로 묶었는데 이젠 모든 컴포넌트를 '세로 스택 뷰'로 묶자.

     

    배치된 모든 컴포넌트를 선택한 후 'Embed in -> Stack View'를 선택하여 스택 뷰로 묶어 보자.

     

     

     

    '세로 스택 뷰'를 선택하고 하단의 정렬 조건 아이콘을 클릭한 후 'Horizontally in Container'에 체크하여 수평을 가운데 정렬로 적용하자.

     

     

    하단의 제약 조건 아이콘을 클릭하여 제약 조건(위:40)을 입력하고 'Add 1 Constraint' 버튼을 클릭하자.

     

     

    그러면 아래 사진처럼 수평 가운데 정렬과 수직 여백이 적용된 모습을 볼 수 있다.

     

     

    프로그레스 뷰를 선택하고 하단의 제약 조건 아이콘을 클릭하여 제약 조건(너비: 290)을 입력하고 'Add 1 Constraint' 버튼을 클릭하자.

     

     

    currentTime, endTime 두 레이블을 선택하고 하단의 제약 조건 아이콘을 클릭하여 제약 조건(너비: 100)을 입력하고 'Add 2 Constraints' 버튼을 클릭하자.

     

     

    currentTime을 선택한 후 오른쪽의 'Size inspector' 버튼을 클릭한 후 Width를 'Edit'로 선택한 후 '='를 '≥'로 변경하자.

     

     

    같은 방법으로 endTime을 선택한 후 오른쪽의 'Size inspector' 버튼을 클릭한 후 Width를 'Edit'로 선택한 후 '='를 '≥'로 변경하자.

     

     

     

    슬라이더를 선택하고 하단의 제약 조건 아이콘을 클릭하여 제약 조건(너비: 220)을 입력하고 'Add 1 Constraint' 버튼을 클릭하자.

     

     

    도큐먼트 아웃라인 영역에서 적용된 제약 조건들을 확인할 수 있다.

     

     

     

    다음은 아이폰 15 Pro와 가장작은 사이즈의 아이폰 SE3을 실행한 결과이다.

     

     

     

    오디오 재생을 위한 아웃렛 변수와 액션 함수 추가하기

     

    프로그레스 뷰(Progress View)를 마우스 오른쪽 버튼으로 클릭한 후 드래그해서 아웃렛 변수를 추가하자.

     

    스토리보드에서 선택하기 힘들면 아래처럼 도큐먼트 아웃라인 영역에서 끌어와 추가해도 된다.

     

    이름은 'pvProgressPlay' 타입은 'UIProgressView'로 지정하자.

     

     

    위와 같은 방법으로 currentTime과 endTime 각각의 아웃렛 변수를 생성하자.

     

    이름은 각각 'lblCurrentTime', 'lblEndTime'로 설정하자.

     

     

     

    이번엔 버튼 3개의 아웃렛 변수를 추가해보자.

     

    추가하는 방법은 위에서 한 것과 동일하다.

     

    이름은 각각 'btnPlay', 'btnPause', 'btnStop'으로 설정하자.

     

     

     

    계속해서 슬라이더 아웃렛 변수를 추가하자.

     

    이름은 'slVolume'로 지정하자.

     

     

     

    아웃렛 변수는 모두 추가했으니 이제 액션 함수를 추가해보자.

     

    Play 버튼의 액션 함수를 아래와 같이 추가하자.

     

    이름은 'btnPlayAudio', 타입은 'UIButton'으로 지정하자.

     

     

    다른 버튼들의 액션 함수도 위와 같은 방법으로 추가하자.

     

    이름은 각각 'btnPauseAudio', 'btnStopAudio'로 지정하자.

     

     

     

    마지막으로 슬라이더의 액션 함수를 추가하자.

     

    이름은 'slChangeVolume', 타입은 'UISilder'로 지정하자.

     

     

     

    오디오 재생을 위한 초기화하기

     

    왼쪽 내비게이터 영역에서 'ViewController.swift' 파일을 선택하고 열자.

     

    오디오를 재생하려면 헤더파일과 델리게이트가 필요하므로 'AVFoundation'을 불러오고 'AVAudioPlayerDelegate' 선언을 추가하자.

     

     

     

    클래스에서 사용할 변수와 상수를 선언해보자.

     

    1. AVAudioPlayer 인스턴스 변수
    2. 재생할 오디오의 파일명 변수
    3. 최대 볼륨, 실수형 상수
    4. 타이머를 위한 변수

     

     

    재생할 음악 mp3 파일을 내비게이터 영역에 끌어다 추가하자.

     

     

     

    viewDidLoad 함수의 audioFile 변수를 방금 추가한 'Sicilian_Breeze.mp3'로 설정하자.

     

     

     

    오디오 재생을 초기화하는 과정을 따로 함수로 만들어보자.

     

    아래 함수를 viewDidLoad 함수 아래에 따로 추가하자.

     

     

    viewDidLoad 함수에 방금 작성한 initPlay 함수를 추가하자.

     

     

     

    initPlay 함수 안에서 앞서 초기화한 audioFile을 URL로 하는 audioPlayer 인스턴스를 생성하자.

     

    이때 AVAudioPlayer 함수는 입력 파라미터인 오디오 파일이 없을 때에 대비하여 do-try-catch 문을 사용하자.

     

     

     

    이제 오디오를 재생할 때 필요한 모든 값을 초기화해보자.

     

    1. 슬라이더(slVolume)의 최대 볼륨을 상수 MAX_VOLUME인 10.0으로 초기화한다.
    2. 슬라이더(slVolume)의 볼륨을 1.0으로 초기화한다.
    3. 프로그레스 뷰(pvPogressPlay)의 진행을 0으로 초기화한다.
    4. audioPlayer의 델리게이트를 self로 한다.
    5. prepareToPlay()를 실행한다.
    6. audioPlayer의 볼륨을 방금 앞에서 초기화한 슬라이더(slVolume)의 볼륨 값 1.0으로 초기화한다.

     

     

     

    재생 시간 초기화하기

     

    'endTime' 레이블인 lblEndTime에 총 재생 시간(오디오 곡 길이)을 나타내기 위해 lblEndTime을 초기화해보자.

     

    아직 코드를 완성하지 않아서 에러가 뜨는데 잠시 후에 코드를 완성할 것이니 지금은 무시하고 넘어가자.

     

     

    오디오의 총 재생 시간인 audioPlayer.duration을 직접 사용하고 싶지만 시간 형태가 초 단위 실수 값이므로 "00:00" 형태로 바꾸는 함수를 만들어야 한다.

     

    "00:00" 형태로 바꾸기 위해 TimeInterval 값을 받아 문자열(String)로 돌려보내는 함수 convertNSTimeInterval2String를 생성하자.

     

     

    convertNSTimeInterval2String 함수 안에 구체적인 코드를 입력하자.

     

    1. 재생 시간의 매개변수인 time 값을 60으로 나눈 '몫'을 정수 값으로 변환하여 상수 min 값에 초기화한다.
    2. time 값을 60으로 나눈 '나머지' 값을 정수 값으로 변환하여 상수 sec 값에 초기화한다.
    3. 이 두 값을 활용해 "%02d:%02d" 형태의 문자열(String)로 변환하여 상수 strTime에 초기화한다.
    4. 이 값을 호출한 함수로 돌려보낸다.

     

     

    초기화한 값을 'endTime' 레이블인 lblEndTime에 나타내보자.

     

    1. 오디오 파일의 재생 시간인 audioPlayer.duration 값을 convertNSTimeInterval2String 함수를 이용해 lblEndTime의 텍스트에 출력한다.
    2. lblCurrentTime의 텍스트에는 convertNSTimeInterval2String 함수를 이용해 00:00가 출력되도록 0의 값을 입력한다.

     

     

    [재생], [일시 정지], [정지] 버튼 제어하기

     

    재생 시간을 초기화했으니 이제는 버튼들을 제어해 보자.

     

    Play 버튼은 오디오를 재생하는 역할을 하고 다른 두 버튼은 오디오를 멈추게 한다.

     

    그러므로 재생에 관한 함수인 initPlay 함수에 Play 버튼은 활성화, 나머지 두 버튼은 비활성화하도록 코드를 추가하자.

     

     

     

    Play, Pause 그리고 Stop 버튼의 동작 여부를 설정하는 부분은 앞으로도 계속 사용해야 하므로 함수를 따로 만들어 사용하자.

     

    이렇게 만든 함수에 아래처럼 재생(Play), 일시 정지(Pause), 정지(Stop)의 순으로 true, false 값을 주면서 각각 설정할 것이다.

     

     

     

    아까 추가했던 코드인 btnPlay.isEnabled = true, btnPause.isEnabled = false, btnStop.isEnabled = false 를 삭제하고 setPlayButtons 함수를 사용하여 다음과 같이 대체하자.

     

    이렇게 하면 의미는 같지만 코드가 간략해진다.

     

     

     

    Play 버튼을 눌러 정상적으로 재생된다면 Play 버튼은 비활성화되고 나머지 두 버튼은 활성화되어야 한다. 이 모습을 구현해보자.

     

    음악을 재생해야 하므로 btnPlayAudio 함수를 수정하자.

     

    1. audioPlayer.play 함수를 실행해 오디오를 재생한다.
    2. Play 버튼은 비활성화, 나머지 두 버튼은 활성화한다.

     

     

    재생을 구현한 것과 비슷한 방식으로 일시 정지를 구현해보자.

     

    오디오를 잠시 멈추도록(일시 정지하도록) btnPauseAudio 함수를 수정하자.

     

    일시 정지 중이므로 audioPlayer.pause 함수를 실행하고 Pause 버튼은 비활성화, 나머지 두 버튼은 활성화한다.

     

     

     

    마지막으로 같은 방식으로 정지를 구현해보자. 오디오를 멈추도록 btnStopAudio 함수를 수정하자. 정지 상태이므로 audioPlayer.stop 함수를 실행하고 Play 버튼은 활성화, 나머지 두 버튼은 비활성화한다.

     

     

     

    이제 시뮬레이터를 실행해보자.

     

    총 재생 시간은 01:55초로 표시되고 재생 시간은 00:00으로 표시된다.

    Play 버튼을 클릭하면 오디오가 재생되고 Play 버튼은 비활성화, 나머지 버튼은 활성화된다. Pause와 Stop 버튼도 잘 동작한다. 하지만 재생 시간은 00:00으로 변화가 없는 모습이다.

     

     

    아이폰 SE3에서도 동일하게 동작하는 것을 확인할 수 있다.

     

     

     

     

    재생 시간 표시하고 볼륨 제어하기

     

    앞의 결과를 보면 재생 시간은 00:00으로 변화가 없었다. 타이머(NSTimer)를 이용하여 재생 시간이 제대로 작동되도록 구현해보자.

     

    우선 btnPlayAudio 함수를 수정하자.

     

    프로그레스 타이머(ProgressTimer)에 Timer.scheduledTimer 함수를 사용하여 0.1초 간격으로 타이머를 생성하도록 구현해보자.

    셀렉터(Selector)는 앞에서 선언한 상수 timePlayerSelector를 사용한다.

    상수 사용 시 에러가 발생하지만 바로 셀렉터 상수를 선언하여 해결할 수 있다.

     

     

    아웃렛 변수를 선언한 위치 바로 위에 재생 타이머를 위한 상수를 추가하자.

     

     

    updatePlayTime 함수를 생성하자.

     

    앞에서 만든 타이머에 의해 0.1초 간격으로 이 함수가 실행되는데, 그 때마다 audioPlayer.currentTime, 즉 재생 시간을 레이블 'lblCurrentTime'과 프로그레스 뷰에 나타낸다.

     

    1. 재생 시간인 audioPlayer.currentTime을 레이블 'lblCurrentTime'에 나타낸다.
    2. 프로그레스 뷰인 pvProgressPlay의 진행 상황에 audioPlayer.currentTime을 audioPlayer.duration으로 나눈 값으로 표시한다.

     

     

    재생 중일 때 시간이 표시되도록 만들었으니 이번에는 정지했을 때 시간이 00:00이 되도록 만들어보자.

     

    정지했을 때의 상황이므로 btnStopAudio 함수를 수정하자.

     

    1. 오디오를 정지하고 다시 재생하면 처음부터 재생해야 하므로 audioPlayer.currentTime을 0으로 한다.
    2. 재생 시간도 00:00으로 초기화하기 위해 convertNSTimeInterval2String(0)을 활용한다.
    3. 타이머도 무효화한다.

     

     

    이제는 볼륨을 조절하기 위해 slChangeVolume 함수를 수정해보자.

     

    화면의 슬라이더를 터치해 좌우로 움직으면 볼륨이 조절되도록 할 것이다.

    이 동작을 구현하기 위해 슬라이더인 slVolume의 값을 오디오 플레이어(audioPlayer)의 volume 값에 대입한다.

     

     

     

    마지막으로 오디오 재생이 끝나면 맨 처음 상태로 돌아가도록 함수를 추가해보자.

     

    타이머도 무효화하고 버튼도 다시 정의해야 한다.

    재생이 끝났으므로 Play 버튼은 활성화, 나머지 버튼은 비활성화하자.

     

    1. 타이머를 무효화한다.
    2. Play 버튼은 활성화하고 나머지 버튼은 비활성화한다.

     

     

    다시 시뮬레이터를 실행해 보면 이제 재생 시간도 제대로 표시되고 볼륨 조절도 가능하다.

     

    그리고 오디오가 종료되면 Play, Pause, Stop 버튼이 새로 설정된다.

     

    (동영상에는 소리가 담기지 않았지만 실행했을 때는 정상적으로 소리가 나왔다.)

     

     

     

     

    오디오 앱 전체 소스 코드

     

    import UIKit
    import AVFoundation
    
    class ViewController: UIViewController, AVAudioPlayerDelegate {
        
        var audioPlayer: AVAudioPlayer!
        var audioFile: URL!
        
        let MAX_VOLUME: Float = 10.0
        
        var progressTimer: Timer!
        
        let timePlayerSelector: Selector = #selector(ViewController.updatePlayTime)
    
        @IBOutlet var pvProgressPlay: UIProgressView!
        @IBOutlet var lblCurrentTime: UILabel!
        @IBOutlet var lblEndTime: UILabel!
        @IBOutlet var btnPlay: UIButton!
        @IBOutlet var btnPause: UIButton!
        @IBOutlet var btnStop: UIButton!
        @IBOutlet var slVolume: UISlider!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            audioFile = Bundle.main.url(forResource: "Sicilian_Breeze", withExtension: "mp3")
            initPlay()
        }
        
        func initPlay() {
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: audioFile)
            } catch let error as NSError {
                print("Error-initPlay : \(error)")
            }
            slVolume.maximumValue = MAX_VOLUME
            slVolume.value = 1.0
            pvProgressPlay.progress = 0
            
            audioPlayer.delegate = self
            audioPlayer.prepareToPlay()
            audioPlayer.volume = slVolume.value
            
            lblEndTime.text = convertNSTimeInterval2String(audioPlayer.duration)
            lblCurrentTime.text = convertNSTimeInterval2String(0)
            
            setPlayButtons(true, pause: false, stop: false)
        }
        
        func setPlayButtons(_ play: Bool, pause: Bool, stop: Bool) {
            btnPlay.isEnabled = play
            btnPause.isEnabled = pause
            btnStop.isEnabled = stop
        }
        
        func convertNSTimeInterval2String(_ time: TimeInterval) -> String {
            let min = Int(time/60)
            let sec = Int(time.truncatingRemainder(dividingBy: 60))
            let strTime = String(format: "%02d:%02d", min, sec)
            return strTime
        }
    
        @IBAction func btnPlayAudio(_ sender: UIButton) {
            audioPlayer.play()
            setPlayButtons(false, pause: true, stop: true)
            progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: timePlayerSelector, userInfo: nil, repeats: true)
        }
        
        @objc func updatePlayTime() {
            lblCurrentTime.text = convertNSTimeInterval2String(audioPlayer.currentTime)
            pvProgressPlay.progress = Float(audioPlayer.currentTime/audioPlayer.duration)
        }
        
        @IBAction func btnPauseAudio(_ sender: UIButton) {
            audioPlayer.pause()
            setPlayButtons(true, pause: false, stop: true)
        }
        
        @IBAction func btnStopAudio(_ sender: UIButton) {
            audioPlayer.stop()
            audioPlayer.currentTime = 0
            lblCurrentTime.text = convertNSTimeInterval2String(0)
            setPlayButtons(true, pause: false, stop: false)
            progressTimer.invalidate()
        }
        
        @IBAction func slChangeVolume(_ sender: UISlider) {
            audioPlayer.volume = slVolume.value
        }
        
        func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
            progressTimer.invalidate()
            setPlayButtons(true, pause: false, stop: false)
        }
        
    }