Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] 에러 핸들링 개선 #35

Merged
merged 9 commits into from
Feb 2, 2025
80 changes: 42 additions & 38 deletions alsongDalsong/ASAudioKit/ASAudioAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,49 @@ import Foundation

public enum ASAudioAnalyzer {
public static func analyze(data: Data, samplesCount: Int) async throws -> [CGFloat] {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".m4a")
try data.write(to: tempURL)
let file = try AVAudioFile(forReading: tempURL)

guard
let format = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: file.fileFormat.sampleRate,
channels: file.fileFormat.channelCount,
interleaved: false
),
let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(file.length))
else {
return []
do {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".m4a")
try data.write(to: tempURL)
let file = try AVAudioFile(forReading: tempURL)

guard
let format = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: file.fileFormat.sampleRate,
channels: file.fileFormat.channelCount,
interleaved: false
),
let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(file.length))
else {
return []
}

try file.read(into: buffer)
guard let floatChannelData = buffer.floatChannelData else {
return []
}

let frameLength = Int(buffer.frameLength)
let samples = Array(UnsafeBufferPointer(start: floatChannelData[0], count: frameLength))
var result = [CGFloat]()
let chunkedSamples = samples.chunked(into: samples.count / samplesCount)

for chunk in chunkedSamples {
let squaredSum = chunk.reduce(0) { $0 + $1 * $1 }
let averagePower = squaredSum / Float(chunk.count)
let decibels = 10 * log10(max(averagePower, Float.ulpOfOne))

let newAmplitude = 1.8 * pow(10.0, decibels / 20.0)
let clampedAmplitude = min(max(CGFloat(newAmplitude), 0), 1)
result.append(clampedAmplitude)
}

try? FileManager.default.removeItem(at: tempURL)

return result
} catch {
throw ASAudioErrors.analyzeError(reason: error.localizedDescription)
Copy link

@gen-com gen-com Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 건우님, 잘 지내고 계신가요 ? 잠깐 구경 왔습니다.

음성 파일을 분석하는 코드를 꼼꼼히 잘 짜주셨는데, 너무 길이가 길지 않나요 ?

데이터를 파일로 쓰는 부분, 파일로 쓴 데이터를 읽어와 청크 단위로 분석하는 부분을 나누면 어떤가요 ?
가독성이 더 올라갈 수 있을 것 같습니다.

궁금한 점이 있다면 데이터를 파일로 변환한 다음에 분석해야하는 이유가 있나요 ?

}

try file.read(into: buffer)
guard let floatChannelData = buffer.floatChannelData else {
return []
}

let frameLength = Int(buffer.frameLength)
let samples = Array(UnsafeBufferPointer(start: floatChannelData[0], count: frameLength))
var result = [CGFloat]()
let chunkedSamples = samples.chunked(into: samples.count / samplesCount)

for chunk in chunkedSamples {
let squaredSum = chunk.reduce(0) { $0 + $1 * $1 }
let averagePower = squaredSum / Float(chunk.count)
let decibels = 10 * log10(max(averagePower, Float.ulpOfOne))

let newAmplitude = 1.8 * pow(10.0, decibels / 20.0)
let clampedAmplitude = min(max(CGFloat(newAmplitude), 0), 1)
result.append(clampedAmplitude)
}

try? FileManager.default.removeItem(at: tempURL)

return result
}
}

Expand Down
24 changes: 24 additions & 0 deletions alsongDalsong/ASAudioKit/ASAudioErrors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

enum ASAudioErrors: Error, LocalizedError {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalizedError가 이미 Error 프로토콜을 채택하고 있습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 케이스가 이유를 가진다면 구조체로 만드는 것은 어땟을지요 ?

case startPlayingError(reason: String)
case getDurationError(reason: String)
case configureAudioSessionError(reason: String)
case startRecordingError(reason: String)
case analyzeError(reason: String)

var errorDescription: String? {
switch self {
case .startPlayingError(let reason):
return "ASAudioPlayer.swift startPlaying() 에러: 오디오 객체 생성에 실패했습니다.\n\(reason)"
case .getDurationError(let reason):
return "ASAudioPlayer.swift getDuration() 에러: 오디오 객체 생성에 실패했습니다.\n\(reason)"
case .configureAudioSessionError(let reason):
return "confitureAudioSession() 에러: 세션 설정에 실패했습니다.\n\(reason)"
case .startRecordingError(let reason):
return "ASAudioRecorder.swift startRecording() 에러: 오디오 레코더 객체 생성에 실패했습니다.\n\(reason)"
case .analyzeError(let reason):
return "ASAudioAnalyzer.swift analyze() 에러: 오디오 분석에 실패했습니다.\n\(reason)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
4E8907C22CE2489A00D5B547 /* ASAudioKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */; platformFilter = ios; };
4EB1ED372CE88E500012FFBA /* ASAudioKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */; };
4EB1ED382CE88E500012FFBA /* ASAudioKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
8593659D2D3F77FE0086C8C4 /* ASAudioErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8593659C2D3F77F60086C8C4 /* ASAudioErrors.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -50,6 +51,7 @@
4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASAudioKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4E8907BE2CE2489A00D5B547 /* ASAudioKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASAudioKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4EB1EC802CE7AA160012FFBA /* ASAudioDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASAudioDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
8593659C2D3F77F60086C8C4 /* ASAudioErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASAudioErrors.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
Expand Down Expand Up @@ -125,6 +127,7 @@
isa = PBXGroup;
children = (
1B7EB01A2CFDA83B00B2BE2A /* ASAudioAnalyzer.swift */,
8593659C2D3F77F60086C8C4 /* ASAudioErrors.swift */,
4E8907AD2CE240D400D5B547 /* ASAudioKit */,
4E8907BF2CE2489A00D5B547 /* ASAudioKitTests */,
4EB1EC812CE7AA160012FFBA /* ASAudioDemo */,
Expand Down Expand Up @@ -305,6 +308,7 @@
buildActionMask = 2147483647;
files = (
1B7EB01C2CFDA83B00B2BE2A /* ASAudioAnalyzer.swift in Sources */,
8593659D2D3F77FE0086C8C4 /* ASAudioErrors.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ public actor ASAudioPlayer: NSObject {
public var onPlaybackFinished: (@Sendable () async -> Void)?

/// 녹음파일을 재생하고 옵션에 따라 재생시간을 설정합니다.
public func startPlaying(data: Data, option: PlayType = .full) {
configureAudioSession()
public func startPlaying(data: Data, option: PlayType = .full) throws {
do {
try configureAudioSession()
audioPlayer = try AVAudioPlayer(data: data)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
audioPlayer?.isMeteringEnabled = true
} catch {
// TODO: 오디오 객체생성 실패 시 처리
throw ASAudioErrors.startPlayingError(reason: error.localizedDescription)
}

switch option {
case .full:
audioPlayer?.play()
case let .partial(time):
audioPlayer?.currentTime = 0
audioPlayer?.play()
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(time)) {
Task {
await self.stopPlaying()
}
case .full:
audioPlayer?.play()
case let .partial(time):
audioPlayer?.currentTime = 0
audioPlayer?.play()
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(time)) {
Task {
await self.stopPlaying()
}
}
}
}

Expand All @@ -51,12 +52,13 @@ public actor ASAudioPlayer: NSObject {
}

/// 녹음파일의 총 녹음시간을 리턴합니다.
public func getDuration(data: Data) -> TimeInterval {
public func getDuration(data: Data) throws -> TimeInterval {
if audioPlayer == nil {
do {
audioPlayer = try AVAudioPlayer(data: data)
} catch {
// TODO: 오디오 객체생성 실패 시 처리
throw ASAudioErrors.getDurationError(reason: error.localizedDescription)
}
}
return audioPlayer?.duration ?? 0
Expand All @@ -66,13 +68,14 @@ public actor ASAudioPlayer: NSObject {
onPlaybackFinished = handler
}

private func configureAudioSession() {
private func configureAudioSession() throws {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
// TODO: 세션 설정 실패에 따른 처리
throw ASAudioErrors.configureAudioSessionError(reason: error.localizedDescription)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,34 @@ public actor ASAudioRecorder {

public init() {}
/// 녹음 후 저장될 파일의 위치를 지정하여 녹음합니다.
public func startRecording(url: URL) {
configureAudioSession()
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
]

public func startRecording(url: URL) throws {
do {
try configureAudioSession()
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
]
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
audioRecorder?.prepareToRecord()
audioRecorder?.isMeteringEnabled = true
audioRecorder?.record()
} catch {
// TODO: AVAudioRecorder 객체 생성 실패 시에 대한 처리
throw ASAudioErrors.startRecordingError(reason: error.localizedDescription)
}
}

/// 오디오 세션을 설정합니다.
private func configureAudioSession() {
private func configureAudioSession() throws {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
// TODO: 세션 설정 실패에 따른 처리
throw ASAudioErrors.configureAudioSessionError(reason: error.localizedDescription)
}
}

Expand Down
4 changes: 4 additions & 0 deletions alsongDalsong/ASDecoder/ASDecoder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
1B06307A2CE64E7C005300BF /* ASDecoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E612CE5193F00124F73 /* ASDecoder.framework */; };
1B06307B2CE64E7C005300BF /* ASDecoder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E612CE5193F00124F73 /* ASDecoder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1B702E6A2CE5193F00124F73 /* ASDecoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E612CE5193F00124F73 /* ASDecoder.framework */; };
8593659A2D3F71E80086C8C4 /* ASDecoderErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859365982D3F71E40086C8C4 /* ASDecoderErrors.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -47,6 +48,7 @@
1B06306B2CE64E74005300BF /* ASDecoderDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASDecoderDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
1B702E612CE5193F00124F73 /* ASDecoder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASDecoder.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1B702E692CE5193F00124F73 /* ASDecoderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASDecoderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
859365982D3F71E40086C8C4 /* ASDecoderErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASDecoderErrors.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -104,6 +106,7 @@
1B702E572CE5193F00124F73 = {
isa = PBXGroup;
children = (
859365982D3F71E40086C8C4 /* ASDecoderErrors.swift */,
1B702E632CE5193F00124F73 /* ASDecoder */,
1B702E6D2CE5193F00124F73 /* ASDecoderTests */,
1B06306C2CE64E74005300BF /* ASDecoderDemo */,
Expand Down Expand Up @@ -285,6 +288,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8593659A2D3F71E80086C8C4 /* ASDecoderErrors.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
12 changes: 8 additions & 4 deletions alsongDalsong/ASDecoder/ASDecoder/ASDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import Foundation

public enum ASDecoder {
public static func decode<T: Decodable>(_: T.Type, from data: Data) throws -> T {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

return try decoder.decode(T.self, from: data)
return try decoder.decode(T.self, from: data)
} catch {
throw ASDecoderErrors.decodeError(reason: error.localizedDescription)
}
}

public static func handleResponse<T: Decodable>(result: Result<Data, Error>) async throws -> T {
Expand Down
12 changes: 12 additions & 0 deletions alsongDalsong/ASDecoder/ASDecoderErrors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

enum ASDecoderErrors: Error, LocalizedError {
case decodeError(reason: String)

var errorDescription: String? {
switch self {
case .decodeError(let reason):
return "ASDecoder.swift decode() 에러: 데이터 디코딩 중 오류가 발생했습니다.\n\(reason)"
}
}
}
4 changes: 4 additions & 0 deletions alsongDalsong/ASEncoder/ASEncoder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
1B1327F02CE5F26400EF706D /* ASEncoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E2F2CE5152600124F73 /* ASEncoder.framework */; };
1B1327F12CE5F26400EF706D /* ASEncoder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E2F2CE5152600124F73 /* ASEncoder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1B702E382CE5152600124F73 /* ASEncoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E2F2CE5152600124F73 /* ASEncoder.framework */; };
859365972D3F71390086C8C4 /* ASEncoderErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859365952D3F71310086C8C4 /* ASEncoderErrors.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -47,6 +48,7 @@
1B702E2F2CE5152600124F73 /* ASEncoder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASEncoder.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1B702E372CE5152600124F73 /* ASEncoderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASEncoderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
1BDDABBD2CE5E2E000147881 /* ASEncoderDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASEncoderDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
859365952D3F71310086C8C4 /* ASEncoderErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASEncoderErrors.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
Expand Down Expand Up @@ -104,6 +106,7 @@
1B702E252CE5152600124F73 = {
isa = PBXGroup;
children = (
859365952D3F71310086C8C4 /* ASEncoderErrors.swift */,
1B702E312CE5152600124F73 /* ASEncoder */,
1B702E3B2CE5152600124F73 /* ASEncoderTests */,
1BDDABBE2CE5E2E000147881 /* ASEncoderDemo */,
Expand Down Expand Up @@ -278,6 +281,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
859365972D3F71390086C8C4 /* ASEncoderErrors.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
14 changes: 9 additions & 5 deletions alsongDalsong/ASEncoder/ASEncoder/ASEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import Foundation

public enum ASEncoder {
public static func encode<T: Encodable>(_ value: T) throws -> Data {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.keyEncodingStrategy = .useDefaultKeys
encoder.outputFormatting = [.prettyPrinted]
do {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.keyEncodingStrategy = .useDefaultKeys
encoder.outputFormatting = [.prettyPrinted]

return try encoder.encode(value)
return try encoder.encode(value)
} catch {
throw ASEncoderErrors.encodeError(reason: error.localizedDescription)
}
}
}
12 changes: 12 additions & 0 deletions alsongDalsong/ASEncoder/ASEncoderErrors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

enum ASEncoderErrors: Error, LocalizedError {
case encodeError(reason: String)

var errorDescription: String? {
switch self {
case .encodeError(let reason):
return "ASEncoder.swift encode() 에러: 데이터 인코딩 중 오류가 발생했습니다.\n\(reason)"
}
}
}
Loading