Skip to content

ScutiUY/ios-wanted-VoiceRecorder

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽ™๏ธ Voice Recorder

๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ ํŒ€์› ์†Œ๊ฐœ

UY ์—๋ฆฌ์–ผ

๐Ÿ–ฅ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

๋…น์Œ ๋ฉ”๋ชจ๋ฅผ ๊ธฐ๋กํ•˜๊ณ , ํ™•์ธํ•˜๋Š” APP

  • ์ฒซ ํ™”๋ฉด์—์„œ ๋…น์Œ๋œ Voice List ํ™•์ธ
  • ํ”Œ๋Ÿฌ์Šค ๋ฒ„ํŠผ์„ ์ด์šฉํ•ด ๋…น์Œ ๊ธฐ๋Šฅ ์ง„์ž…
  • ๋…น์Œ ์ง„ํ–‰ ์‹œ Frequency ์กฐ์ ˆํ•˜๋ฉฐ ๋…น์Œ ๊ฐ€๋Šฅ
  • ๋…น์Œ ํ›„ ์žฌ์ƒ ํ™•์ธ
  • 5์ดˆ ์ „ํ›„ ์žฌ์ƒ ๊ธฐ๋Šฅ
  • ์žฌ์ƒ ์‹œ PitchControl ๊ธฐ๋Šฅ
  • ์žฌ์ƒ ํŒŒํ˜• ํ™•์ธ
  • FirebaseStorage Clound

โฑ๏ธ ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„ ๋ฐ ์‚ฌ์šฉ ๊ธฐ์ˆ 

  • ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„: 2022.06.27 ~ 2022.07.09 (2์ฃผ)
  • ์‚ฌ์šฉ ๊ธฐ์ˆ : UIKit, FirebaseStorage, AVAudioEngine, AVAudioUnitEQ, AVFAudio, Accelerate, MVC

๐Ÿ–ผ ๋””์ž์ธ ํŒจํ„ด

MVVM? MVC?

  • MVC๋ฅผ ์„ ํƒํ•œ ์ด์œ 
  1. ๊ทœ๋ชจ๊ฐ€ ํฌ์ง€ ์•Š์€ ํ”„๋กœ์ ํŠธ์—์„œ ๋ณด์—ฌ์ค„ ๋ทฐ์˜ ์ˆ˜๊ฐ€ ๋งŽ์ง€ ์•Š์Œ โœ…

  2. ๊ธฐ๋Šฅ์˜ ์ง๊ด€์ ์ธ ๋ถ„๋ฆฌ

  3. Model๊ณผ View๊ฐ€ ๋‹ค๋ฅธ ๊ณณ์— ์ข…์†๋˜์ง€ ์•Š์Œ โ†’ ํ™•์žฅ์˜ ํŽธ๋ฆฌ์„ฑ


๐Ÿ“Œย ํ•ต์‹ฌ ๊ธฐ์ˆ 

  • AudioEngine์„ ์ด์šฉํ•œ ๋…น์Œ๊ณผ ์žฌ์ƒ

  • ์†Œ๋ฆฌ ํŒŒํ˜• ๋‚ด๋ถ€์—์„œ์˜ ์Šคํฌ๋กค

  • ์˜ค๋””์˜ค์™€ Visualizer ์—ฐ๋™

  • Network ์ฒ˜๋ฆฌ


โญ ์ƒˆ๋กœ ๋ฐฐ์šด ๊ฒƒ

AVAudioEngine์„ ์‚ฌ์šฉํ•œ Audio Data ์ฒ˜๋ฆฌ

AVAudioUnitEQ๋ฅผ ์ด์šฉํ•œ Frequency ์ฒ˜๋ฆฌ

Firebase Cloud Storage๋ฅผ ์ด์šฉํ•œ ๋…น์Œ ํŒŒ์ผ ์ €์žฅ์†Œ

Cloud์™€ Local์˜ Data upload & download & delete ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Custom View ๊ตฌํ˜„ ๋ฐ ์‚ฌ์šฉ


๐Ÿ“–ย DataFlow

VoiceRecoder


โš ๏ธ ์ด์Šˆ

  • Visualizer ๊ตฌํ˜„์‹œ scrollView ๋‚ด๋ถ€์—์„œ Layer๋ฅผ ๊ทธ๋ฆด์‹œ scrollView contentSize๋ฅผ ๋Š˜๋ ค๋„ ์ •๋ฐฉํ–ฅ์œผ๋กœ ๋Š˜์–ด๋‚จ์œผ๋กœ ์ธํ•ด ์›ํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์Šคํฌ๋กค ๋ถˆ๊ฐ€

    โ†’ CGAffineTransform() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐ๋ฅผ ๋ฐ˜์ „ ์‹œ์ผœ์คŒ

// ์Šคํฌ๋กค์„ ๋‹ด๋‹นํ•˜๋Š” AudioVisualizeView ์ดˆ๊ธฐํ™” ๋ถ€๋ถ„
init(playType: PlayType) {
        super.init(frame: .zero)
        // ...
        switch playType {
        case .playback:
            self.transform = CGAffineTransform(scaleX: 1, y: -1)
        case .record:
            self.transform = CGAffineTransform(scaleX: -1, y: 1)
        }
        // ...
    }

// ์ง์ ‘ ๋ ˆ์ด์–ด๋ฅผ ๊ทธ๋ฆฌ๋Š” AudioPlotView
init(playType: PlayType) {
        // ...
        switch playType {
        case .playback:
            self.transform = CGAffineTransform(scaleX: 1, y: -1)
        case .record:
            self.transform = CGAffineTransform(scaleX: -1, y: 1)
        }
        // ...
    }

  • ๊ธฐ์กด ๋ฐฉ์‹: ์„œ๋ฒ„์—์„œ downloadAllRef()๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ฃผ์†Œ๋ฅผ ๊ฐ€์ ธ์™€ ๊ฐœ๋ณ„ ๋ฐ์ดํ„ฐ ํ†ต์‹  ์„ฑ๊ณต์‹œ๋งˆ๋‹ค ๋ฐ˜ํ™˜

    โ†’ ์„ฑ๊ณตํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ ํ•˜์—ฌ ํ•œ๋ฒˆ์— ๋ทฐ์—์„œ ์ฒ˜๋ฆฌ

func downloadAllRef(completion: @escaping ([StorageReference]) -> Void) {
        baseReference.listAll { [unowned self] result, error in
            if let error = error {
                delegate.firebaseStorageManager(error: error, desc: .allReferenceFailed)
            }
            if let result = result {
                completion(result.items)
            }
        }
    }

func downloadMetaData(filePath: [StorageReference], completion: @escaping ([AudioMetaData]) -> Void) {
        
        var audioMetaDataList = [AudioMetaData]()
        
        for ref in filePath {
            baseReference.child(ref.name).getMetadata { [unowned self] metaData, error in
                if let error = error {
                    delegate.firebaseStorageManager(error: error, desc: .MetaDataFailed)
                }
                
                let data = metaData?.customMetadata
                let title = data?["title"] ?? ""
                let duration = data?["duration"] ?? "00:00"
                let url = data?["url"] ?? title + ".caf"
                let waveforms = data?["waveforms"]?.components(separatedBy: " ").map{Float($0)!} ?? []
                audioMetaDataList.append(AudioMetaData(title: title, duration: duration, url: url, waveforms: waveforms))
                
                if audioMetaDataList.count == filePath.count {
                    completion(audioMetaDataList)
                }
            }
        }
    }

  • Visualizer๋ฅผ ํฌํ•จํ•œ VC์—์„œ present ๋  ์‹œ layer๋ฅผ ๊ทธ๋ฆฌ๋Š” ๋ทฐ ์ง€์ •์ด ์ œ๋Œ€๋กœ ๋˜์ง€ ์•Š๋Š” ์ด์Šˆ

    โ†’ DispatchQueue๋ฅผ ํ†ตํ•ด์„œ view๊ฐ€ ์˜ฌ๋ผ์˜ฌ๋•Œ 0.01์ดˆ๋ฅผ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ๊ทธ๋ ค์คŒ์œผ๋กœ์จ ํ•ด๊ฒฐ

DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 1) { [self] in
		DispatchQueue.main.async { [self] in
		    visualizer.setWaveformData(waveDataArray: audioData.waveforms)
        visualEffectView.removeFromSuperview()
        loadingIndicator.stopAnimating()
    }
}
loadingIndicator.startAnimating()

๐Ÿ’ผ ๋ฆฌํŒฉํ† ๋ง

  • ๋ฉ”์ธ VC๋ฅผ ์ ‘๊ทผํ•˜์—ฌ VoiceList ํ‘œ์‹œ ์‹œ, ์ „์ฒด ํŒŒ์ผ์˜ metaData๋ฅผ downloadํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹

    โ†’ ์ฒซ ์ง„์ž…์‹œ๋งŒ ๋‹ค์šด ๋ฐ›๊ณ , ์ดํ›„ ๋…น์Œ๋˜๋Š” ํŒŒ์ผ์€ delegate Pattern์œผ๋กœ metaData๋งŒ ๋„˜๊ฒจ VoiceList์— ์ถ”๊ฐ€ํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹

protocol PassMetaDataDelegate {
    func sendMetaData(audioMetaData: AudioMetaData)
}

class RecordViewController: UIViewController {
    
    var delegate: PassMetaDataDelegate!
        // ...
        

        private func passData(localUrl : URL) {
        let data = try! Data(contentsOf: localUrl)
        let totalTime = soundManager.totalPlayTime(date: date)
        let duration = soundManager.convertTimeToString(totalTime)
        let audioMetaData = AudioMetaData(title: date, duration: duration, url: urlString)
        
        firebaseStorageManager.uploadAudio(audioData: data, audioMetaData: audioMetaData)
        delegate.sendMetaData(audioMetaData: audioMetaData)
    }

        // ...
}
extension RecordedVoiceListViewController: PassMetaDataDelegate {
    
    func sendMetaData(audioMetaData: AudioMetaData) {
        audioMetaDataList.append(audioMetaData)
        sortAudioFiles()
        recordedVoiceTableView.reloadData()
    }
}

  • ๋…น์Œ์ด ๋๋‚˜๊ณ  uploadAudio์— ๋„˜๊ฒจ์ฃผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋‹จ์ผ๊ฐ’์œผ๋กœ ๊ฐ๊ฐ ๋ณด๋‚ด๊ณ  ๋กœ์ง ์ˆ˜ํ–‰

    1. ๊ทธ๋Ÿฌ๋‹ค๋ณด๋‹ˆ ๊ฐ๊ฐ์˜ class์—์„œ ๋ฐ›์€ ๋งŽ์€ ์—ญํ• ์„ ์ˆ˜ํ–‰
    2. SOLID ์›์น™ ์ค‘ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™(SRP)์— ์œ„๋ฐฐ

    โ†’ AudioMetaData Model์„ ๋งŒ๋“ค์–ด ๊ฐ’์„ ๋‹ด์•„์„œ ๋ณด๋ƒ„

func uploadAudio(audioData: Data, audioMetaData: AudioMetaData) {
        let title = audioMetaData.title
        let duration = audioMetaData.duration
        let filePath = audioMetaData.url
        let waveforms = audioMetaData.waveforms.map{String($0)}.joined(separator: " ")
        let metaData = StorageMetadata()
        let customData = [
        "title": title,
        "duration": duration,
        "url": filePath,
        "waveforms": waveforms
      ]

        metaData.customMetadata = customData
    metaData.contentType = "audio/x-caf"

    baseReference.child(filePath).putData(audioData, metadata: metaData) { [unowned self] metaData, error in
        if let error = error {
            delegate.firebaseStorageManager(error: error, desc: .uploadFailed)
            return
        }
    }
}

๐Ÿ“ฑ UI

๋…น์Œ & ๋ฆฌ์ŠคํŠธ ์—…๋ฐ์ดํŠธ ์žฌ์ƒ 5์ดˆ ์ „ํ›„
Simulator Screen Recording - iPhone 11 Pro - 2022-07-09 at 23 54 00 Simulator Screen Recording - iPhone 11 Pro - 2022-07-09 at 23 55 00 Simulator Screen Recording - iPhone 11 Pro - 2022-07-09 at 23 52 48

About

Wanted Pre_Onboarding 1st Assignment

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 100.0%