iOS 메트로놈 개발 삽질기, 안드로이드 개발자의 48시간 사투
프로젝트 배경
Flutter로 개발한 Metronome 앱에서 안드로이드는 완벽하게 동작하는데, iOS에서만 온갖 문제가 발생하는 상황. iOS 네이티브와 사투를 벌인 48시간의 기록.
초기 상황
- 안드로이드: SoundPool로 완벽 구현, 배포까지 완료
- iOS: 소리도 안나고, 나와도 크래시, 나와도 느림
- 목표: 안드로이드와 동일한 성능과 안정성
1단계: iOS 기본 구현 (처음부터 막힘)
문제 1: 소리가 아예 안남
// 처음 시도 - 당연히 안됨
func playSound() {
// 어떻게 소리를 내지...?
}
Swift해결: Bundle에서 Flutter assets 찾는 방법 학습
// Flutter assets 접근 방법 발견
guard let appFrameworkPath = Bundle.main.path(
forResource: "Frameworks/App",
ofType: "framework"
),
let appBundle = Bundle(path: appFrameworkPath) else {
return
}
let assetPath = "flutter_assets/assets/\(fileName)"
let url = appBundle.url(forResource: assetPath, withExtension: nil)
Swift문제 2: .mp3에서 .wav로 전환
처음엔 안드로이드와 동일하게 .mp3 사용했는데…
- 문제: iOS에서 .mp3 로딩이 불안정
- 해결: 모든 오디오 파일을 .wav로 변환
// pubspec.yaml도 수정
final Map<String, String> soundFiles = {
'click': 'sounds/click.wav', // .mp3 → .wav
'wood': 'sounds/wood.wav',
'bell': 'sounds/bell.wav',
'cat': 'sounds/cat.wav',
};
Dart2단계: Accent 사운드 구현 (볼륨 vs 피치 대결)
초기 접근: 볼륨으로 악센트 구분
피치로 악센트를 주는 것이 이상하게 IOS는 구현하기가 까다로워 볼륨으로 접근 시도.
let volume = isAccent ? accentVolume : normalVolume
Swift볼륨으로 악센트를 주기엔 악센트 느낌이 안나서 실패
AVAudioUnitTimePitch 시도 (실패)
// 피치 조절 시도 - 복잡하고 불안정
private var timePitchUnit: AVAudioUnitTimePitch
timePitchUnit.pitch = isAccent ? 1200 : 0 // cents 단위
Swift문제: 음질 깨짐, 복잡한 구조, 여전히 크래시
최종 해결: _accent.wav 파일 분리
아이디어: 볼륨/피치 조절 대신 아예 별도 파일 사용
// 직접 로직으로 Pitch가 다른 음을 .wav 파일로 export 함.
let accentFileName = fileName.replacingOccurrences(of: ".wav", with: "_accent.wav")
// 예: click.wav + click_accent.wav
// cat.wav + cat_accent.wav
Swift결과: 완벽한 음질, 사용자가 원하는 정확한 피치 차이
3단계: AVAudioEngine 크래시 지옥
첫 번째 구현: AVAudioEngine + AVAudioPlayerNode
private let audioEngine = AVAudioEngine()
private var playerNodes: [AVAudioPlayerNode] = []
private var audioBuffers: [String: AVAudioPCMBuffer] = [:]
// 크래시의 주범
playerNode.scheduleBuffer(buffer, at: nil, options: [], completionHandler: nil)
Swift크래시 로그:
Runner.debug.dylib MetronomeEngine.playSoundSafely(volume:pitch:) + 1872 (MetronomeEngine.swift:533)
Plaintext시도한 해결책들:
- 안전성 체크 추가 – 여전히 크래시
- 버퍼 풀링 – 여전히 크래시
- 메모리 관리 개선 – 여전히 크래시
- 스레드 안전성 – 여전히 크래시
4단계: AVAudioPlayer 시도 (성능 지옥)
풀링 방식 구현
private var audioPlayerPool: [AVAudioPlayer] = []
private let poolSize = 6 // 겹치는 소리 방지용
// 각 플레이어 생성에 400ms씩...
for i in 0..<poolSize {
let player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay()
audioPlayerPool.append(player)
}
Swift성능 문제:
- 초기화: 4개 사운드 × 6개 풀 × 400ms = 10초 로딩
- 사운드 변경: 매번 2.4초 대기
단일 플레이어 시도
private var audioPlayer: AVAudioPlayer?
// 매번 새로 생성
audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
Swift여전한 문제: accent 소리를 위해 임시 플레이어 생성 시 렉
5단계: SystemSound 발견 (빛과 그림자)
첫 번째 SystemSound 구현
private var systemSoundID: SystemSoundID = 0
// 드디어 빠른 재생!
AudioServicesPlaySystemSound(systemSoundID)
Swift- 장점: 즉시 로딩, 즉시 재생
- 단점: 빠른 연속 재생에서 소리 씹힘
연속 재생 문제
SystemSound는 이전 소리가 끝나기 전에 새 소리 재생 시 씹힘
6단계: 최종 해결책 – SystemSound 멀티 인스턴스
핵심 아이디어: 풀링으로 씹힘 방지
// 여러 개의 동일한 SystemSound 생성
private var systemSoundIDs: [SystemSoundID] = []
private var accentSoundIDs: [SystemSoundID] = []
private var currentSoundIndex = 0
private let soundPoolSize = 4
// 로딩: 같은 소리를 4번 등록
for i in 0..<soundPoolSize {
var soundID: SystemSoundID = 0
AudioServicesCreateSystemSoundID(audioURL as CFURL, &soundID)
systemSoundIDs.append(soundID)
}
// 재생: 순환하며 사용
let soundToPlay = soundsToUse[currentSoundIndex]
AudioServicesPlaySystemSound(soundToPlay)
currentSoundIndex = (currentSoundIndex + 1) % soundsToUse.count
Swift최종 결과
Before vs After
항목Before (AVAudioEngine)After (SystemSound 풀)
로딩 시간 | 10초+ | 즉시 |
재생 지연 | 수백ms | 거의 없음 |
크래시 | 빈번 | 전혀 없음 |
고속 재생 | 불가능 | 완벽 |
메모리 사용 | 높음 | 최소 |
성능 테스트 통과
- 180+ BPM: 문제없음
- 6연음: 소리 씹힘 없음
- meow 사운드: 완벽 재생
- 사운드 변경: 즉시 적용
핵심 교훈
1. “간단한 게 최고다”
복잡한 AVAudioEngine보다 단순한 SystemSound가 더 안정적
2. “성능 vs 기능 트레이드오프”
- SystemSound: 볼륨 조절 불가, 하지만 빠르고 안정적
- 해결: _accent.wav 파일로 우회
3. “플랫폼별 특성 이해”
- Android: SoundPool이 최적
- iOS: SystemSound가 최적
- 같은 Flutter 앱이라도 네이티브는 다르게 구현
결론
48시간의 사투 끝에: 안드로이드만큼 완벽하게 동작하는 iOS 메트로놈 완성!
때로는 가장 단순한 API가 가장 강력한 솔루션이 될 수 있다는 것을 배웠다. iOS 오디오 개발에서는 적절한 API 선택이 성패를 좌우한다.
핵심: 복잡하게 갈 필요 없이, SystemSound + 멀티 인스턴스 풀링이면 충분!