// // BackgroundAudioKeeper.swift // Keging // // 운동 중 무음 오디오 루프 재생 — 앱을 나가도 타이머·진동·Live Activity 갱신이 유지되게 한다. // (UIBackgroundModes=audio, .mixWithOthers라 음악 재생을 방해하지 않음) // 인터럽션(전화 등)·백그라운드 전환에서 재생이 끊기면 다시 살린다. // import AVFoundation import UIKit @MainActor final class BackgroundAudioKeeper { static let shared = BackgroundAudioKeeper() private var player: AVAudioPlayer? private var observers: [NSObjectProtocol] = [] private init() {} func start() { guard player == nil else { return } let session = AVAudioSession.sharedInstance() try? session.setCategory(.playback, mode: .default, options: [.mixWithOthers]) try? session.setActive(true) player = try? AVAudioPlayer(data: Self.silenceWAV) player?.numberOfLoops = -1 // 0.0은 일부 환경에서 백그라운드 유지가 풀리는 사례가 있어 소량으로 — 샘플이 무음이라 들리지 않는다 player?.volume = 0.1 player?.prepareToPlay() player?.play() installObservers() } func stop() { removeObservers() player?.stop() player = nil try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation]) } /// 재생이 죽었으면 되살린다 (인터럽션 종료·씬 전환 시) private func revive() { guard let player, !player.isPlaying else { return } try? AVAudioSession.sharedInstance().setActive(true) player.play() } private func installObservers() { guard observers.isEmpty else { return } let center = NotificationCenter.default observers.append(center.addObserver( forName: AVAudioSession.interruptionNotification, object: nil, queue: .main ) { [weak self] note in guard let self, let info = note.userInfo, let rawType = info[AVAudioSessionInterruptionTypeKey] as? UInt, AVAudioSession.InterruptionType(rawValue: rawType) == .ended else { return } Task { @MainActor in self.revive() } }) for name in [UIApplication.didEnterBackgroundNotification, UIApplication.didBecomeActiveNotification] { observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in guard let self else { return } Task { @MainActor in self.revive() } }) } } private func removeObservers() { observers.forEach { NotificationCenter.default.removeObserver($0) } observers.removeAll() } /// 1초짜리 무음 WAV (8kHz 16bit mono) — 파일 없이 메모리에서 생성 private static let silenceWAV: Data = { let sampleRate = 8000 let dataSize = sampleRate * 2 var d = Data() func append(_ s: String) { d.append(s.data(using: .ascii)!) } func appendU32(_ v: UInt32) { withUnsafeBytes(of: v.littleEndian) { d.append(contentsOf: $0) } } func appendU16(_ v: UInt16) { withUnsafeBytes(of: v.littleEndian) { d.append(contentsOf: $0) } } append("RIFF"); appendU32(UInt32(36 + dataSize)); append("WAVE") append("fmt "); appendU32(16); appendU16(1); appendU16(1) appendU32(UInt32(sampleRate)); appendU32(UInt32(sampleRate * 2)); appendU16(2); appendU16(16) append("data"); appendU32(UInt32(dataSize)) d.append(Data(count: dataSize)) return d }() }