UY | ์๋ฆฌ์ผ |
---|---|
- ์ฒซ ํ๋ฉด์์ ๋ น์๋ Voice List ํ์ธ
- ํ๋ฌ์ค ๋ฒํผ์ ์ด์ฉํด ๋ น์ ๊ธฐ๋ฅ ์ง์
- ๋ น์ ์งํ ์ Frequency ์กฐ์ ํ๋ฉฐ ๋ น์ ๊ฐ๋ฅ
- ๋ น์ ํ ์ฌ์ ํ์ธ
- 5์ด ์ ํ ์ฌ์ ๊ธฐ๋ฅ
- ์ฌ์ ์ PitchControl ๊ธฐ๋ฅ
- ์ฌ์ ํํ ํ์ธ
- FirebaseStorage Clound
- ๊ฐ๋ฐ ๊ธฐ๊ฐ: 2022.06.27 ~ 2022.07.09 (2์ฃผ)
- ์ฌ์ฉ ๊ธฐ์ :
UIKit
,FirebaseStorage
,AVAudioEngine
,AVAudioUnitEQ
,AVFAudio
,Accelerate
,MVC
- MVC๋ฅผ ์ ํํ ์ด์
-
๊ท๋ชจ๊ฐ ํฌ์ง ์์ ํ๋ก์ ํธ์์ ๋ณด์ฌ์ค ๋ทฐ์ ์๊ฐ ๋ง์ง ์์ โ
-
๊ธฐ๋ฅ์ ์ง๊ด์ ์ธ ๋ถ๋ฆฌ
-
Model๊ณผ View๊ฐ ๋ค๋ฅธ ๊ณณ์ ์ข ์๋์ง ์์ โ ํ์ฅ์ ํธ๋ฆฌ์ฑ
-
AudioEngine์ ์ด์ฉํ ๋ น์๊ณผ ์ฌ์
-
์๋ฆฌ ํํ ๋ด๋ถ์์์ ์คํฌ๋กค
-
์ค๋์ค์ Visualizer ์ฐ๋
-
Network ์ฒ๋ฆฌ
AVAudioEngine์ ์ฌ์ฉํ Audio Data ์ฒ๋ฆฌ
AVAudioUnitEQ๋ฅผ ์ด์ฉํ Frequency ์ฒ๋ฆฌ
Firebase Cloud Storage๋ฅผ ์ด์ฉํ ๋ น์ ํ์ผ ์ ์ฅ์
Cloud์ Local์ Data upload & download & delete ๋ถ๊ธฐ ์ฒ๋ฆฌ
์ฌ์ฌ์ฉ ๊ฐ๋ฅํ Custom View ๊ตฌํ ๋ฐ ์ฌ์ฉ
-
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์ ๋๊ฒจ์ฃผ๋ ํ๋ผ๋ฏธํฐ๋ฅผ ๋จ์ผ๊ฐ์ผ๋ก ๊ฐ๊ฐ ๋ณด๋ด๊ณ ๋ก์ง ์ํ
- ๊ทธ๋ฌ๋ค๋ณด๋ ๊ฐ๊ฐ์ class์์ ๋ฐ์ ๋ง์ ์ญํ ์ ์ํ
- 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
}
}
}
๋ น์ & ๋ฆฌ์คํธ ์ ๋ฐ์ดํธ | ์ฌ์ | 5์ด ์ ํ |
---|---|---|