// // BackgroundAudioKeeper.swift // Keging // // 운동 중 무음 오디오 루프 재생 — 앱을 나가도 타이머·진동·Live Activity 갱신이 유지되게 한다. // (UIBackgroundModes=audio, .mixWithOthers라 음악 재생을 방해하지 않음) // import AVFoundation @MainActor final class BackgroundAudioKeeper { static let shared = BackgroundAudioKeeper() private var player: AVAudioPlayer? 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 player?.volume = 0 player?.play() } func stop() { player?.stop() player = nil try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation]) } /// 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 }() }