본문 바로가기
2023년 이전/swift

music player 만들기

by JeongUPark 2020. 9. 20.
반응형

뮤직플레이어를 만들어야할 기회가 생겨서 한번 만들어 보았습니다.

 

전체 코드는 여기서 확인 하실 수 있습니다.

 

1. 앨범 별로 화면에 노출하기

 

우선 첫번쨰 화면은 iTunes Library에서 음악 정보를 가져와서 앨범별로 구분하여 보여주는 화면입니다.

 

이 작업을 할때  첫번째로 막혔던 부분은 iTunes Library에서 어떻게 음악 정보를 가져오는지 였습니다. 이럴 땐 구글링을 통하여 정보를 확인하였습니다.

 

iTunes Library에서 음악정보를 가져오는 방법은 다음과 같습니다.

import MediaPlayer

class SongQuery {

    func get(songCategory: String) -> [AlbumInfo] {

        var albums: [AlbumInfo] = []
        let albumsQuery: MPMediaQuery
        albumsQuery = MPMediaQuery.albums()

        let albumItems: [MPMediaItemCollection] = albumsQuery.collections! as [MPMediaItemCollection]
        
        // ....
        }
 }

위처럼 코딩을 하면 albumItems에 iTunes Library로 부터 album 단위로 음악 정보를 가져올 수 있습니다.

그리고 나서  다음과 같이 그 정보를 파싱 합니다.

for album in albumItems {

    let albumItems: [MPMediaItem] = album.items as [MPMediaItem]

    var songs: [SongInfo] = []

    var albumTitle: String = ""
    var albumArtist : String = ""
    var albumartwork : UIImage? = nil

    for song in albumItems {
   
        albumTitle = song.value( forProperty: MPMediaItemPropertyAlbumTitle ) as! String
        albumArtist = song.value( forProperty: MPMediaItemPropertyArtist ) as! String
        if let artwork: MPMediaItemArtwork = song.value(forProperty: MPMediaItemPropertyArtwork) as? MPMediaItemArtwork{
            albumartwork = artwork.image(at: CGSize(width: 200, height: 200))
        }

        let songInfo: SongInfo = SongInfo(
            albumTitle: song.value( forProperty: MPMediaItemPropertyAlbumTitle ) as! String,
            artistName: song.value( forProperty: MPMediaItemPropertyArtist ) as! String,
            songTitle:  song.value( forProperty: MPMediaItemPropertyTitle ) as! String,
            songId:     song.value( forProperty: MPMediaItemPropertyPersistentID ) as! NSNumber,
            songURL:  song.value(forKey: MPMediaItemPropertyAssetURL) as! NSURL,
            trackNum: song.value(forProperty: MPMediaItemPropertyAlbumTrackCount) as! NSNumber,
            albumartwork: albumartwork
        )
        songs.append( songInfo )
    }

    let albumInfo: AlbumInfo = AlbumInfo(

        albumTitle: albumTitle,
        albumArtist: albumArtist,
        albumartwork: albumartwork,
        songs: songs
    )

    albums.append( albumInfo )
}

앨범 별로 구분하여 음악 정보와 앨범 정보 데이터를 만들어 줍니다.

 

사실 이렇게 만들면 다 될줄 알았는데 다음의 문제들이 있었습니다.

1. 음악 파일에 대한 테스트는 시뮬레이터에서 할 수 없어서 디바이스로만 가능하다.

2. iTunes Library에 접근하기 위해 음악 접근 퍼미션을 주어야 한다.

그래서 퍼미션을 주기위해 우선 info.list에 다음을 추가하고

MPMediaLibrary.requestAuthorization { (status) in
            if status == .authorized {
                self.albums = self.songQuery.get(songCategory: "")
                DispatchQueue.main.async {
                    if self.albums.count == 0 {
                        self.noAlbumLabel.isHidden = false
                        self.albumCollectionView.isHidden = true
                    }else{
                        self.albumCollectionView.reloadData()
                    }
                }
            }
        }

이렇게 퍼미션을 승인 받습니다.

퍼미션이 승인되면 아까 위에서 만든 sonQuery로 부터 음악 정보를 받고 이를 화면에 뿌려줍니다. 화면은 colletcionView를 사용하여 그리드 형태로 나타나도록 작업하였습니다.

 

그리고 위에서 DispatchQueue.main.async를 사용하여 collectionView에 대한 작업을 하였는데, 그 이유는 MPMediaLibrary.requestAuthorization 이 부분이 아마 main Thread가 아니라서 실행 후 collectionView가 그려지는데 까지 오랜 시간이 필요하게 됩니다. DispatchQueue.main.async를 사용하면 그 시간이 사라집니다.!(그리고 xcode에서도 main thread에서 작업하라고 경고해 줍니다.)

 

 

2. 음악 재생

 

다음은 음악재생입니다.

 

음악 재생은 구분된 앨범을 누르면 SongList가 나타나고 상단에 재생, 반복, 셔플 버튼이 존재, 재생을 누르면 처음부터, 각 음악을 누르면 거기서 부터 음악이 재생 됩니다.

 

음악관리를 한곳에서 하기위해 음악 전용의 Sigleton class를 만들고, 각 앨범을 클릭시 관련 음악 리스트를 sigleton class에 전달하여 관리하도록 하였습니다.

 

Sigleton class의 내용은 다음과 같습니다.

//
//  MusicHelper.swift
//  musicPlayer_for_ios
//
//  Created by JeongU Park on 2020/09/17.
//  Copyright © 2020 JeongU Park. All rights reserved.
//

import Foundation
import AVFoundation
class MusicHelper : NSObject, AVAudioPlayerDelegate{
    
    static let sharedHelper = MusicHelper()
    var songList : [SongInfo]!
    var songInfo : SongInfo!
    var backgroundMusicPlayer: AVAudioPlayer!
    var isShuffle : Bool = false
    var isRepeat : Bool = false
    var currentPlayNum = 0
    func setinit(songlist : [SongInfo], currentNum : Int){
        self.songList = songlist
        self.currentPlayNum = currentNum
    }
    func setCurrentPlayNum(num : Int){
        self.currentPlayNum = num
    }
    func playBackgroundMusic( isNewMusic : Bool = false){
        
        self.songInfo = self.songList[self.currentPlayNum]
        let url = self.songInfo.songURL
        do {
            backgroundMusicPlayer =  try AVAudioPlayer(contentsOf: url  as URL )
            backgroundMusicPlayer.numberOfLoops = 0
            backgroundMusicPlayer.delegate = self
            if isNewMusic {
                backgroundMusicPlayer.prepareToPlay()
            }
            backgroundMusicPlayer.play()
        }catch{
            print(error)
        }
    }
    func pauseMusic(){
        self.backgroundMusicPlayer.pause()
    }
    func checkisPlaying() -> Bool{
        if self.backgroundMusicPlayer != nil {
            return self.backgroundMusicPlayer.isPlaying
        }else{
            return false
        }
    }
    func getCurrentSongInfo() -> SongInfo {
        return songInfo
    }
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool){
        print("finish")
        stopMusic()
    }
    
    func stopMusic(){
        print("stopMusic isShuffle: \(isShuffle) | isRepeat : \(isRepeat)")
        if isShuffle {
            var isCheck = true
            while(isCheck){
                let changeNum  = Int.random(in: 0 ..< songList.count)
                if currentPlayNum != changeNum {
                    currentPlayNum = changeNum
                    isCheck = false
                }
            }
            print("stopMusic currentPlayNum: \(currentPlayNum) ")
            playBackgroundMusic()
            return
        }
        if isRepeat {
            currentPlayNum += 1
            if currentPlayNum >= songList.count {
                currentPlayNum = 0
            }
            print("stopMusic currentPlayNum: \(currentPlayNum) ")
            playBackgroundMusic()
        }
        
        
    }
    
}

이곳에서 작업할 때 힘들었던 점은 

backgroundMusicPlayer.numberOfLoops 의 값을 0이 아니라 -1로 했더니 한곡만 무한 반복되고 다른 곳으로 넘어가지 않는 문제였습니다.  ( numberOfLoops의 내용을 보니 

   /* "numberOfLoops" is the number of times that the sound will return to the beginning upon reaching the end. 
    A value of zero means to play the sound just once.
    A value of one will result in playing the sound twice, and so on..
    Any negative number will loop indefinitely until stopped.
    */
    open var numberOfLoops: Int

이렇게 얼마나 반복할지 그리고 마이너스면 stop이 될때까지 무한 반복이 었습니다.)

 

그래서 음악이 자동종료 될떄 그 정보를 체크하는 audioPlayerDidFinishPlaying 가 호출되지 않아 머가 문제지 계속 고민을 하는데 많은 시간을 허비 하였습니다.

 

3. 미니플레이어 만들기

미니플레이어의 경우 미니플레이어용 View를 따로 만들어서 각 Controller에서 viewWillAppear 에서 음악이 재생중이라면 화면에 addview로 미니플레이를 추가 하였습니다.

그리고 음악 재생은 

//
//  MiniPlayerView.swift
//  musicPlayer_for_ios
//
//  Created by JeongU Park on 2020/09/17.
//  Copyright © 2020 JeongU Park. All rights reserved.
//

import Foundation
import UIKit
import AVFoundation
import MediaPlayer

class MiniPlayerView : RoundedBoarderView {
    
    @IBOutlet weak var musicProgress: UIProgressView!
    @IBOutlet weak var pauseBtn: UIButton!
    @IBOutlet weak var songtitle: UILabel!
    @IBOutlet weak var artist: UILabel!
    @IBOutlet weak var albmImg: UIImageView!
    let timePlayerSelector:Selector = #selector(updatePlayTime)
    var progressTimer : Timer!
    var musicHelper = MusicHelper.sharedHelper
    var viewController : UIViewController!
    func setViewController(_ viewController : UIViewController){
        self.viewController = viewController
    }
    override func awakeFromNib() {
        songtitle.text = musicHelper.getCurrentSongInfo().songTitle
        artist.text = musicHelper.getCurrentSongInfo().artistName
        if let img = musicHelper.getCurrentSongInfo().albumartwork {
            albmImg.image = img
        }else{
            albmImg.image = UIImage(named: "noartwork.png")
        }
        
        musicProgress.progress = 0
        progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: timePlayerSelector, userInfo: nil, repeats: true)
    }

    @IBAction func clickPause(_ sender: Any) {
        musicHelper.backgroundMusicPlayer.pause()
    }
    @objc func updatePlayTime() {
        musicProgress.progress = Float(musicHelper.backgroundMusicPlayer.currentTime/musicHelper.backgroundMusicPlayer.duration)
        if musicHelper.backgroundMusicPlayer.currentTime == 0 {
            musicProgress.progress = 0
            songtitle.text = musicHelper.getCurrentSongInfo().songTitle
            artist.text = musicHelper.getCurrentSongInfo().artistName
        }
    }
    @IBAction func clickMiniPlayer(_ sender: Any) {
        print("click MiniPlayer")
        let vc = self.viewController.storyboard?.instantiateViewController(withIdentifier: "CurrentPlayViewController") as! CurrentPlayViewController
        self.viewController.present(vc, animated: true, completion: nil)
        
    }
}

Timer를 사용하여 음악 재생을 표현해 주었습니다.

 

4. 재생중인 음악 상세화면

재생중인 음악 상세화면의 경우 미니플레이어와 비슷하지만, 음악 빨리 감기 / 되감기 기능이 추가되어있고, 음악 리스트에 있는 반복과 셔플 버튼도 존재 합니다. 그리고 볼륨도 조정 할 수 있었습니다.

 

이 작업에서 힘들었던 점은 볼륨입니다.

 

처음에는 그냥 Slider를 사용하여 볼륨 조절을 했었는데, 이게 앱에서는 동작이 되는데, 문제는 시스템 볼륨이 변경 되지 않아 고민을 하였습니다. 

 

그래서 이 역시 구글링의 힘으로 작업

 let mpview = MPVolumeView(frame: CGRect(x: 20, y: volumContorlY, width: self.view.frame.width - 80, height: 30))
 self.view.addSubview(mpview)

MPVolumeVIew를 추가하였더니, 동일한 Slider가 나타났고, 이를 조작하니 시스템 볼륨까지 변경 되었습니다.

 

5. 백그라운드 재생

백그라운드 재생을 위해서는 다음과 같은 작업을 해주어야 합니다.

이곳에서 Background Module에 Audio, AirPlay, and Picture in Picture 부분을 체크해주며 백그라운드에서 음악이 재생 됩니다.

 

 

 

사실은 음악 control도 시스템과 연결되어 동작해야하는데 그 방법은 아무리 해봐도 되지 않아서 계속 연구 중에 있습니다.

 

 

반응형

'2023년 이전 > swift' 카테고리의 다른 글

클로저  (0) 2020.08.30
구문 이름표  (0) 2020.07.12
swift의switch 문  (0) 2020.07.12
튜플,배열, 딕셔너리,세트, 열거형  (0) 2020.07.05