SoundFile.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. // LoopFollow
  2. // SoundFile.swift
  3. import Foundation
  4. /// A sound chosen for an alarm.
  5. ///
  6. /// `.builtin` wraps the filename (no extension) of a `.caf` resource bundled with the app.
  7. /// `.custom` references an audio file imported by the user into `Documents/CustomSounds/`,
  8. /// keyed by UUID (see `CustomSoundStore`).
  9. ///
  10. /// Codable note: built-in values encode/decode as a bare string (e.g. `"Indeed"`) so stored
  11. /// alarm data written by older app versions continues to load. Custom values encode as a
  12. /// keyed object and are new with this version.
  13. enum SoundFile: Hashable, Identifiable {
  14. case builtin(String)
  15. case custom(UUID)
  16. var id: String {
  17. switch self {
  18. case let .builtin(name): return "builtin:\(name)"
  19. case let .custom(uuid): return "custom:\(uuid.uuidString)"
  20. }
  21. }
  22. /// Human-friendly name for display in pickers.
  23. var displayName: String {
  24. switch self {
  25. case let .builtin(name):
  26. return name
  27. .replacingOccurrences(of: "_", with: " ")
  28. .replacingOccurrences(of: " ", with: " ")
  29. case let .custom(uuid):
  30. return CustomSoundStore.shared.displayName(for: uuid) ?? "Custom Sound"
  31. }
  32. }
  33. /// Convenience for call sites that still think in terms of a bundle filename.
  34. var builtinName: String? {
  35. if case let .builtin(name) = self { return name }
  36. return nil
  37. }
  38. /// Back-compat shim for migration code that reads legacy string values out of UserDefaults.
  39. /// The string is treated as a built-in filename; playback will fall back gracefully if the
  40. /// file isn't actually in the bundle.
  41. init?(rawValue: String) {
  42. guard !rawValue.isEmpty else { return nil }
  43. self = .builtin(rawValue)
  44. }
  45. }
  46. extension SoundFile: Codable {
  47. private enum CodingKeys: String, CodingKey {
  48. case kind, name, id
  49. }
  50. init(from decoder: Decoder) throws {
  51. // Legacy form: bare string == built-in filename.
  52. if let container = try? decoder.singleValueContainer(),
  53. let name = try? container.decode(String.self)
  54. {
  55. self = .builtin(name)
  56. return
  57. }
  58. let c = try decoder.container(keyedBy: CodingKeys.self)
  59. let kind = try c.decode(String.self, forKey: .kind)
  60. switch kind {
  61. case "builtin":
  62. let name = try c.decode(String.self, forKey: .name)
  63. self = .builtin(name)
  64. case "custom":
  65. let uuid = try c.decode(UUID.self, forKey: .id)
  66. self = .custom(uuid)
  67. default:
  68. throw DecodingError.dataCorruptedError(
  69. forKey: .kind,
  70. in: c,
  71. debugDescription: "Unknown SoundFile kind: \(kind)"
  72. )
  73. }
  74. }
  75. func encode(to encoder: Encoder) throws {
  76. switch self {
  77. case let .builtin(name):
  78. // Preserve legacy wire format so older app versions can still read it.
  79. var c = encoder.singleValueContainer()
  80. try c.encode(name)
  81. case let .custom(uuid):
  82. var c = encoder.container(keyedBy: CodingKeys.self)
  83. try c.encode("custom", forKey: .kind)
  84. try c.encode(uuid, forKey: .id)
  85. }
  86. }
  87. }
  88. // MARK: - Built-in catalog
  89. extension SoundFile {
  90. /// Fallback used when a referenced sound file can't be located (e.g. a deleted custom sound).
  91. static let fallback: SoundFile = .indeed
  92. /// Every built-in sound shipped with the app, in the order shown in the picker.
  93. static let allBuiltins: [SoundFile] = [
  94. .alarmBuzzer, .alarmClock, .alertToneBusy, .alertToneRingtone1, .alertToneRingtone2,
  95. .alienSiren, .ambulance, .analogWatchAlarm, .bigClockTicking, .burglarAlarmSiren1,
  96. .burglarAlarmSiren2, .cartoonAscendClimbSneaky, .cartoonAscendThenDescend,
  97. .cartoonBounceToCeiling, .cartoonDreamyGlissandoHarp, .cartoonFailStringsTrumpet,
  98. .cartoonMachineClumsyLoop, .cartoonSiren, .cartoonTipToeSneakyWalk, .cartoonUhOh,
  99. .cartoonVillainHorns, .cellPhoneRingTone, .chimesGlassy, .computerMagic, .csfx2Alarm,
  100. .cuckooClock, .dholShuffleloop, .discreet, .earlySunrise, .emergencyAlarmCarbonMonoxide,
  101. .emergencyAlarmSiren, .emergencyAlarm, .endingReached, .fly, .ghostHover, .goodMorning,
  102. .hellYeahSomewhatCalmer, .inAHurry, .indeed, .insistently, .jingleAllTheWay, .laserShoot,
  103. .machineCharge, .magicalTwinkle, .marchingHeavyFootedFatElephants, .marimbaDescend,
  104. .marimbaFlutterOrShake, .martianGun, .martianScanner, .metallic, .nightguard, .notKiddin,
  105. .openYourEyesAndSee, .orchestralHorns, .oringz, .pagerBeeps, .remembersMeOfAsia,
  106. .riseAndShine, .rush, .sciFiAirRaidAlarm, .sciFiAlarmLoop1, .sciFiAlarmLoop2,
  107. .sciFiAlarmLoop3, .sciFiAlarmLoop4, .sciFiAlarm, .sciFiComputerConsoleAlarm,
  108. .sciFiConsoleAlarm, .sciFiEerieAlarm, .sciFiEngineShutDown, .sciFiIncomingMessageAlert,
  109. .sciFiSpaceshipMessage, .sciFiSpaceshipWarmUp, .sciFiWarning, .signatureCorporate,
  110. .siriAlertCalibrationNeeded, .siriAlertDeviceMuted, .siriAlertGlucoseDroppingFast,
  111. .siriAlertGlucoseRisingFast, .siriAlertHighGlucose, .siriAlertLowGlucose,
  112. .siriAlertMissedReadings, .siriAlertTransmitterBatteryLow, .siriAlertUrgentHighGlucose,
  113. .siriAlertUrgentLowGlucose, .siriCalibrationNeeded, .siriDeviceMuted,
  114. .siriGlucoseDroppingFast, .siriGlucoseRisingFast, .siriHighGlucose, .siriLowGlucose,
  115. .siriMissedReadings, .siriTransmitterBatteryLow, .siriUrgentHighGlucose,
  116. .siriUrgentLowGlucose, .softMarimbaPadPositive, .softWarmAiryOptimistic,
  117. .softWarmAiryReassuring, .storeDoorChime, .sunny, .thunderSoundFX, .timeHasCome,
  118. .tornadoSiren, .twoTurtleDoves, .unpaved, .wakeUpWillYou, .winGain, .wrongAnswer,
  119. ]
  120. // Static aliases so existing call sites like `soundFile = .indeed` keep compiling.
  121. static let alarmBuzzer: SoundFile = .builtin("Alarm_Buzzer")
  122. static let alarmClock: SoundFile = .builtin("Alarm_Clock")
  123. static let alertToneBusy: SoundFile = .builtin("Alert_Tone_Busy")
  124. static let alertToneRingtone1: SoundFile = .builtin("Alert_Tone_Ringtone_1")
  125. static let alertToneRingtone2: SoundFile = .builtin("Alert_Tone_Ringtone_2")
  126. static let alienSiren: SoundFile = .builtin("Alien_Siren")
  127. static let ambulance: SoundFile = .builtin("Ambulance")
  128. static let analogWatchAlarm: SoundFile = .builtin("Analog_Watch_Alarm")
  129. static let bigClockTicking: SoundFile = .builtin("Big_Clock_Ticking")
  130. static let burglarAlarmSiren1: SoundFile = .builtin("Burglar_Alarm_Siren_1")
  131. static let burglarAlarmSiren2: SoundFile = .builtin("Burglar_Alarm_Siren_2")
  132. static let cartoonAscendClimbSneaky: SoundFile = .builtin("Cartoon_Ascend_Climb_Sneaky")
  133. static let cartoonAscendThenDescend: SoundFile = .builtin("Cartoon_Ascend_Then_Descend")
  134. static let cartoonBounceToCeiling: SoundFile = .builtin("Cartoon_Bounce_To_Ceiling")
  135. static let cartoonDreamyGlissandoHarp: SoundFile = .builtin("Cartoon_Dreamy_Glissando_Harp")
  136. static let cartoonFailStringsTrumpet: SoundFile = .builtin("Cartoon_Fail_Strings_Trumpet")
  137. static let cartoonMachineClumsyLoop: SoundFile = .builtin("Cartoon_Machine_Clumsy_Loop")
  138. static let cartoonSiren: SoundFile = .builtin("Cartoon_Siren")
  139. static let cartoonTipToeSneakyWalk: SoundFile = .builtin("Cartoon_Tip_Toe_Sneaky_Walk")
  140. static let cartoonUhOh: SoundFile = .builtin("Cartoon_Uh_Oh")
  141. static let cartoonVillainHorns: SoundFile = .builtin("Cartoon_Villain_Horns")
  142. static let cellPhoneRingTone: SoundFile = .builtin("Cell_Phone_Ring_Tone")
  143. static let chimesGlassy: SoundFile = .builtin("Chimes_Glassy")
  144. static let computerMagic: SoundFile = .builtin("Computer_Magic")
  145. static let csfx2Alarm: SoundFile = .builtin("CSFX-2_Alarm")
  146. static let cuckooClock: SoundFile = .builtin("Cuckoo_Clock")
  147. static let dholShuffleloop: SoundFile = .builtin("Dhol_Shuffleloop")
  148. static let discreet: SoundFile = .builtin("Discreet")
  149. static let earlySunrise: SoundFile = .builtin("Early_Sunrise")
  150. static let emergencyAlarmCarbonMonoxide: SoundFile = .builtin("Emergency_Alarm_Carbon_Monoxide")
  151. static let emergencyAlarmSiren: SoundFile = .builtin("Emergency_Alarm_Siren")
  152. static let emergencyAlarm: SoundFile = .builtin("Emergency_Alarm")
  153. static let endingReached: SoundFile = .builtin("Ending_Reached")
  154. static let fly: SoundFile = .builtin("Fly")
  155. static let ghostHover: SoundFile = .builtin("Ghost_Hover")
  156. static let goodMorning: SoundFile = .builtin("Good_Morning")
  157. static let hellYeahSomewhatCalmer: SoundFile = .builtin("Hell_Yeah_Somewhat_Calmer")
  158. static let inAHurry: SoundFile = .builtin("In_A_Hurry")
  159. static let indeed: SoundFile = .builtin("Indeed")
  160. static let insistently: SoundFile = .builtin("Insistently")
  161. static let jingleAllTheWay: SoundFile = .builtin("Jingle_All_The_Way")
  162. static let laserShoot: SoundFile = .builtin("Laser_Shoot")
  163. static let machineCharge: SoundFile = .builtin("Machine_Charge")
  164. static let magicalTwinkle: SoundFile = .builtin("Magical_Twinkle")
  165. static let marchingHeavyFootedFatElephants: SoundFile = .builtin("Marching_Heavy_Footed_Fat_Elephants")
  166. static let marimbaDescend: SoundFile = .builtin("Marimba_Descend")
  167. static let marimbaFlutterOrShake: SoundFile = .builtin("Marimba_Flutter_or_Shake")
  168. static let martianGun: SoundFile = .builtin("Martian_Gun")
  169. static let martianScanner: SoundFile = .builtin("Martian_Scanner")
  170. static let metallic: SoundFile = .builtin("Metallic")
  171. static let nightguard: SoundFile = .builtin("Nightguard")
  172. static let notKiddin: SoundFile = .builtin("Not_Kiddin")
  173. static let openYourEyesAndSee: SoundFile = .builtin("Open_Your_Eyes_And_See")
  174. static let orchestralHorns: SoundFile = .builtin("Orchestral_Horns")
  175. static let oringz: SoundFile = .builtin("Oringz")
  176. static let pagerBeeps: SoundFile = .builtin("Pager_Beeps")
  177. static let remembersMeOfAsia: SoundFile = .builtin("Remembers_Me_Of_Asia")
  178. static let riseAndShine: SoundFile = .builtin("Rise_And_Shine")
  179. static let rush: SoundFile = .builtin("Rush")
  180. static let sciFiAirRaidAlarm: SoundFile = .builtin("Sci-Fi_Air_Raid_Alarm")
  181. static let sciFiAlarmLoop1: SoundFile = .builtin("Sci-Fi_Alarm_Loop_1")
  182. static let sciFiAlarmLoop2: SoundFile = .builtin("Sci-Fi_Alarm_Loop_2")
  183. static let sciFiAlarmLoop3: SoundFile = .builtin("Sci-Fi_Alarm_Loop_3")
  184. static let sciFiAlarmLoop4: SoundFile = .builtin("Sci-Fi_Alarm_Loop_4")
  185. static let sciFiAlarm: SoundFile = .builtin("Sci-Fi_Alarm")
  186. static let sciFiComputerConsoleAlarm: SoundFile = .builtin("Sci-Fi_Computer_Console_Alarm")
  187. static let sciFiConsoleAlarm: SoundFile = .builtin("Sci-Fi_Console_Alarm")
  188. static let sciFiEerieAlarm: SoundFile = .builtin("Sci-Fi_Eerie_Alarm")
  189. static let sciFiEngineShutDown: SoundFile = .builtin("Sci-Fi_Engine_Shut_Down")
  190. static let sciFiIncomingMessageAlert: SoundFile = .builtin("Sci-Fi_Incoming_Message_Alert")
  191. static let sciFiSpaceshipMessage: SoundFile = .builtin("Sci-Fi_Spaceship_Message")
  192. static let sciFiSpaceshipWarmUp: SoundFile = .builtin("Sci-Fi_Spaceship_Warm_Up")
  193. static let sciFiWarning: SoundFile = .builtin("Sci-Fi_Warning")
  194. static let signatureCorporate: SoundFile = .builtin("Signature_Corporate")
  195. static let siriAlertCalibrationNeeded: SoundFile = .builtin("Siri_Alert_Calibration_Needed")
  196. static let siriAlertDeviceMuted: SoundFile = .builtin("Siri_Alert_Device_Muted")
  197. static let siriAlertGlucoseDroppingFast: SoundFile = .builtin("Siri_Alert_Glucose_Dropping_Fast")
  198. static let siriAlertGlucoseRisingFast: SoundFile = .builtin("Siri_Alert_Glucose_Rising_Fast")
  199. static let siriAlertHighGlucose: SoundFile = .builtin("Siri_Alert_High_Glucose")
  200. static let siriAlertLowGlucose: SoundFile = .builtin("Siri_Alert_Low_Glucose")
  201. static let siriAlertMissedReadings: SoundFile = .builtin("Siri_Alert_Missed_Readings")
  202. static let siriAlertTransmitterBatteryLow: SoundFile = .builtin("Siri_Alert_Transmitter_Battery_Low")
  203. static let siriAlertUrgentHighGlucose: SoundFile = .builtin("Siri_Alert_Urgent_High_Glucose")
  204. static let siriAlertUrgentLowGlucose: SoundFile = .builtin("Siri_Alert_Urgent_Low_Glucose")
  205. static let siriCalibrationNeeded: SoundFile = .builtin("Siri_Calibration_Needed")
  206. static let siriDeviceMuted: SoundFile = .builtin("Siri_Device_Muted")
  207. static let siriGlucoseDroppingFast: SoundFile = .builtin("Siri_Glucose_Dropping_Fast")
  208. static let siriGlucoseRisingFast: SoundFile = .builtin("Siri_Glucose_Rising_Fast")
  209. static let siriHighGlucose: SoundFile = .builtin("Siri_High_Glucose")
  210. static let siriLowGlucose: SoundFile = .builtin("Siri_Low_Glucose")
  211. static let siriMissedReadings: SoundFile = .builtin("Siri_Missed_Readings")
  212. static let siriTransmitterBatteryLow: SoundFile = .builtin("Siri_Transmitter_Battery_Low")
  213. static let siriUrgentHighGlucose: SoundFile = .builtin("Siri_Urgent_High_Glucose")
  214. static let siriUrgentLowGlucose: SoundFile = .builtin("Siri_Urgent_Low_Glucose")
  215. static let softMarimbaPadPositive: SoundFile = .builtin("Soft_Marimba_Pad_Positive")
  216. static let softWarmAiryOptimistic: SoundFile = .builtin("Soft_Warm_Airy_Optimistic")
  217. static let softWarmAiryReassuring: SoundFile = .builtin("Soft_Warm_Airy_Reassuring")
  218. static let storeDoorChime: SoundFile = .builtin("Store_Door_Chime")
  219. static let sunny: SoundFile = .builtin("Sunny")
  220. static let thunderSoundFX: SoundFile = .builtin("Thunder_Sound_FX")
  221. static let timeHasCome: SoundFile = .builtin("Time_Has_Come")
  222. static let tornadoSiren: SoundFile = .builtin("Tornado_Siren")
  223. static let twoTurtleDoves: SoundFile = .builtin("Two_Turtle_Doves")
  224. static let unpaved: SoundFile = .builtin("Unpaved")
  225. static let wakeUpWillYou: SoundFile = .builtin("Wake_Up_Will_You")
  226. static let winGain: SoundFile = .builtin("Win_Gain")
  227. static let wrongAnswer: SoundFile = .builtin("Wrong_Answer")
  228. }