Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6fd0562a41
commit
2904536334
@ -8,26 +8,60 @@
|
||||
import Foundation
|
||||
|
||||
/// Sound data model for audio files
|
||||
public struct Sound: Identifiable, Hashable {
|
||||
public struct Sound: Identifiable, Hashable, Codable {
|
||||
public let id: String
|
||||
public let name: String
|
||||
public let fileName: String
|
||||
public let category: String
|
||||
public let description: String
|
||||
public let bundleName: String? // Optional bundle name for organization
|
||||
public let isDefault: Bool? // Optional - used for alarm sounds to mark default
|
||||
|
||||
// MARK: - Initialization
|
||||
public init(name: String, fileName: String, category: String, description: String, bundleName: String? = nil) {
|
||||
self.id = fileName // Use fileName as stable identifier
|
||||
public init(id: String? = nil, name: String, fileName: String, category: String, description: String, bundleName: String? = nil, isDefault: Bool? = nil) {
|
||||
self.id = id ?? UUID().uuidString // Use provided id or generate GUID
|
||||
self.name = name
|
||||
self.fileName = fileName
|
||||
self.category = category
|
||||
self.description = description
|
||||
self.bundleName = bundleName
|
||||
self.isDefault = isDefault
|
||||
}
|
||||
|
||||
// MARK: - Codable Custom Implementation
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Generate a new GUID for each sound loaded from JSON
|
||||
self.id = UUID().uuidString
|
||||
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.fileName = try container.decode(String.self, forKey: .fileName)
|
||||
self.category = try container.decode(String.self, forKey: .category)
|
||||
self.description = try container.decode(String.self, forKey: .description)
|
||||
self.bundleName = try container.decodeIfPresent(String.self, forKey: .bundleName)
|
||||
self.isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(fileName, forKey: .fileName)
|
||||
try container.encode(category, forKey: .category)
|
||||
try container.encode(description, forKey: .description)
|
||||
try container.encodeIfPresent(bundleName, forKey: .bundleName)
|
||||
try container.encodeIfPresent(isDefault, forKey: .isDefault)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, name, fileName, category, description, bundleName, isDefault
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,40 +9,17 @@ import Foundation
|
||||
|
||||
/// Configuration model for sound system loaded from JSON
|
||||
public struct SoundConfiguration: Codable {
|
||||
public let sounds: [SoundConfig]
|
||||
public let sounds: [Sound]
|
||||
public let categories: [SoundCategory]
|
||||
public let settings: AudioSettings
|
||||
|
||||
public init(sounds: [SoundConfig], categories: [SoundCategory], settings: AudioSettings) {
|
||||
public init(sounds: [Sound], categories: [SoundCategory], settings: AudioSettings) {
|
||||
self.sounds = sounds
|
||||
self.categories = categories
|
||||
self.settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
/// Individual sound configuration
|
||||
public struct SoundConfig: Codable, Identifiable {
|
||||
public let id: String
|
||||
public let name: String
|
||||
public let fileName: String
|
||||
public let category: String
|
||||
public let description: String
|
||||
public let bundleName: String? // Optional bundle name for organization
|
||||
|
||||
public init(id: String, name: String, fileName: String, category: String, description: String, bundleName: String? = nil) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.fileName = fileName
|
||||
self.category = category
|
||||
self.description = description
|
||||
self.bundleName = bundleName
|
||||
}
|
||||
|
||||
/// Convert to Sound model for compatibility
|
||||
public func toSound() -> Sound {
|
||||
return Sound(name: name, fileName: fileName, category: category, description: description, bundleName: bundleName)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sound category configuration
|
||||
public struct SoundCategory: Codable, Identifiable {
|
||||
@ -89,10 +66,9 @@ public class SoundConfigurationService {
|
||||
private init() {}
|
||||
|
||||
/// Load sound configuration from JSON file
|
||||
public func loadConfiguration(from bundle: Bundle = .main, fileName: String = "sounds") -> SoundConfiguration? {
|
||||
public func loadConfiguration(from bundle: Bundle = .main, fileName: String = "sounds") -> SoundConfiguration {
|
||||
guard let url = bundle.url(forResource: fileName, withExtension: "json") else {
|
||||
print("❌ \(fileName).json not found in bundle")
|
||||
return nil
|
||||
fatalError("❌ \(fileName).json not found in bundle. Ensure the file exists in your app bundle.")
|
||||
}
|
||||
|
||||
do {
|
||||
@ -102,81 +78,43 @@ public class SoundConfigurationService {
|
||||
print("✅ Loaded sound configuration with \(config.sounds.count) sounds")
|
||||
return config
|
||||
} catch {
|
||||
print("❌ Error loading sound configuration: \(error)")
|
||||
return nil
|
||||
fatalError("❌ Error loading sound configuration: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current configuration
|
||||
public func getConfiguration() -> SoundConfiguration? {
|
||||
public func getConfiguration() -> SoundConfiguration {
|
||||
if configuration == nil {
|
||||
return loadConfiguration()
|
||||
}
|
||||
return configuration
|
||||
return configuration!
|
||||
}
|
||||
|
||||
/// Get all available sounds
|
||||
public func getAvailableSounds() -> [Sound] {
|
||||
guard let config = getConfiguration() else {
|
||||
print("⚠️ No configuration available, falling back to constants")
|
||||
return getFallbackSounds()
|
||||
}
|
||||
|
||||
return config.sounds.map { $0.toSound() }
|
||||
return getConfiguration().sounds
|
||||
}
|
||||
|
||||
/// Get sounds by category
|
||||
public func getSoundsByCategory(_ categoryId: String) -> [Sound] {
|
||||
guard let config = getConfiguration() else {
|
||||
return []
|
||||
}
|
||||
|
||||
return config.sounds
|
||||
return getConfiguration().sounds
|
||||
.filter { $0.category == categoryId }
|
||||
.map { $0.toSound() }
|
||||
}
|
||||
|
||||
/// Get sounds by bundle name
|
||||
public func getSoundsByBundle(_ bundleName: String) -> [Sound] {
|
||||
guard let config = getConfiguration() else {
|
||||
return []
|
||||
}
|
||||
|
||||
return config.sounds
|
||||
return getConfiguration().sounds
|
||||
.filter { $0.bundleName == bundleName }
|
||||
.map { $0.toSound() }
|
||||
}
|
||||
|
||||
/// Get alarm sounds specifically
|
||||
public func getAlarmSounds() -> [Sound] {
|
||||
return getSoundsByCategory("alarm")
|
||||
}
|
||||
|
||||
/// Get available categories
|
||||
public func getAvailableCategories() -> [SoundCategory] {
|
||||
return getConfiguration()?.categories ?? []
|
||||
return getConfiguration().categories
|
||||
}
|
||||
|
||||
/// Get audio settings
|
||||
public func getAudioSettings() -> AudioSettings? {
|
||||
return getConfiguration()?.settings
|
||||
public func getAudioSettings() -> AudioSettings {
|
||||
return getConfiguration().settings
|
||||
}
|
||||
|
||||
/// Fallback sounds if JSON loading fails
|
||||
private func getFallbackSounds() -> [Sound] {
|
||||
return [
|
||||
// White noise sounds
|
||||
Sound(name: "White Noise", fileName: "white-noise.mp3", category: "ambient", description: "Classic white noise for focus and relaxation", bundleName: "Ambient"),
|
||||
Sound(name: "Heavy Rain White Noise", fileName: "heavy-rain-white-noise.mp3", category: "nature", description: "Heavy rainfall sounds for peaceful sleep", bundleName: "Nature"),
|
||||
Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater.mp3", category: "mechanical", description: "Fan and heater sounds for consistent background noise", bundleName: "Mechanical"),
|
||||
|
||||
// Alarm sounds
|
||||
Sound(name: "Digital Alarm", fileName: "digital-alarm.mp3", category: "alarm", description: "Classic digital alarm sound", bundleName: "AlarmSounds"),
|
||||
Sound(name: "iPhone Alarm", fileName: "iphone-alarm.mp3", category: "alarm", description: "iPhone-style alarm sound", bundleName: "AlarmSounds"),
|
||||
Sound(name: "Classic Alarm", fileName: "classic-alarm.mp3", category: "alarm", description: "Traditional alarm sound", bundleName: "AlarmSounds"),
|
||||
Sound(name: "Beep Alarm", fileName: "beep-alarm.mp3", category: "alarm", description: "Short beep alarm sound", bundleName: "AlarmSounds"),
|
||||
Sound(name: "Siren Alarm", fileName: "siren-alarm.mp3", category: "alarm", description: "Emergency siren alarm for heavy sleepers", bundleName: "AlarmSounds"),
|
||||
Sound(name: "Voice Wake Up", fileName: "voice-wakeup.mp3", category: "alarm", description: "Voice-based wake up sound", bundleName: "AlarmSounds")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,11 +128,11 @@ public class NoisePlayer {
|
||||
let settings = soundConfigurationService.getAudioSettings()
|
||||
|
||||
// Use configuration settings or fall back to constants
|
||||
let category = settings?.audioSessionCategory == "playback" ?
|
||||
let category = settings.audioSessionCategory == "playback" ?
|
||||
AVAudioSession.Category.playback : AudioConstants.AudioSession.category
|
||||
let mode = settings?.audioSessionMode == "default" ?
|
||||
let mode = settings.audioSessionMode == "default" ?
|
||||
AVAudioSession.Mode.default : AudioConstants.AudioSession.mode
|
||||
let options: AVAudioSession.CategoryOptions = settings?.audioSessionOptions.contains("mixWithOthers") == true ?
|
||||
let options: AVAudioSession.CategoryOptions = settings.audioSessionOptions.contains("mixWithOthers") == true ?
|
||||
[.mixWithOthers] : AudioConstants.AudioSession.options
|
||||
|
||||
try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options)
|
||||
@ -163,9 +163,9 @@ public class NoisePlayer {
|
||||
|
||||
do {
|
||||
let player = try AVAudioPlayer(contentsOf: fileUrl)
|
||||
player.numberOfLoops = settings?.defaultLoopCount ?? AudioConstants.Playback.numberOfLoops
|
||||
player.volume = settings?.defaultVolume ?? AudioConstants.Volume.default
|
||||
if settings?.preloadSounds ?? AudioConstants.Playback.prepareToPlay {
|
||||
player.numberOfLoops = settings.defaultLoopCount
|
||||
player.volume = settings.defaultVolume
|
||||
if settings.preloadSounds {
|
||||
player.prepareToPlay()
|
||||
}
|
||||
players[sound.fileName] = player
|
||||
|
||||
65
TheNoiseClock/Resources/alarm-sounds.json
Normal file
65
TheNoiseClock/Resources/alarm-sounds.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"sounds": [
|
||||
{
|
||||
"id": "digital-alarm",
|
||||
"name": "Digital Alarm",
|
||||
"fileName": "digital-alarm.caf",
|
||||
"description": "Classic digital alarm sound",
|
||||
"category": "alarm",
|
||||
"bundleName": null,
|
||||
"isDefault": true
|
||||
},
|
||||
{
|
||||
"id": "buzzing-alarm",
|
||||
"name": "Buzzing Alarm",
|
||||
"fileName": "buzzing-alarm.caf",
|
||||
"description": "Buzzing sound for gentle wake-up",
|
||||
"category": "alarm",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "classic-alarm",
|
||||
"name": "Classic Alarm",
|
||||
"fileName": "classic-alarm.caf",
|
||||
"description": "Traditional alarm sound",
|
||||
"category": "alarm",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "beep-alarm",
|
||||
"name": "Beep Alarm",
|
||||
"fileName": "beep-alarm.caf",
|
||||
"description": "Short beep alarm sound",
|
||||
"category": "alarm",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "siren-alarm",
|
||||
"name": "Siren Alarm",
|
||||
"fileName": "siren-alarm.caf",
|
||||
"description": "Emergency siren alarm for heavy sleepers",
|
||||
"category": "alarm",
|
||||
"bundleName": null
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "alarm",
|
||||
"name": "Alarm Sounds",
|
||||
"description": "Wake-up and notification alarm sounds",
|
||||
"bundleName": null
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"defaultVolume": 1.0,
|
||||
"defaultLoopCount": -1,
|
||||
"preloadSounds": true,
|
||||
"preloadStrategy": "category",
|
||||
"audioSessionCategory": "playback",
|
||||
"audioSessionMode": "default",
|
||||
"audioSessionOptions": ["mixWithOthers"],
|
||||
"defaultSound": "digital-alarm.caf",
|
||||
"previewDuration": 3.0,
|
||||
"fadeInDuration": 0.5
|
||||
}
|
||||
}
|
||||
@ -23,46 +23,6 @@
|
||||
"category": "mechanical",
|
||||
"description": "Fan and heater sounds for consistent background noise",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "digital-alarm",
|
||||
"name": "Digital Alarm",
|
||||
"fileName": "digital-alarm.caf",
|
||||
"category": "alarm",
|
||||
"description": "Classic digital alarm sound",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "iphone-alarm",
|
||||
"name": "Buzzing Alarm",
|
||||
"fileName": "buzzing-alarm.caf",
|
||||
"category": "alarm",
|
||||
"description": "Buzzing sound",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "classic-alarm",
|
||||
"name": "Classic Alarm",
|
||||
"fileName": "classic-alarm.caf",
|
||||
"category": "alarm",
|
||||
"description": "Traditional alarm sound",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "beep-alarm",
|
||||
"name": "Beep Alarm",
|
||||
"fileName": "beep-alarm.caf",
|
||||
"category": "alarm",
|
||||
"description": "Short beep alarm sound",
|
||||
"bundleName": null
|
||||
},
|
||||
{
|
||||
"id": "siren-alarm",
|
||||
"name": "Siren Alarm",
|
||||
"fileName": "siren-alarm.caf",
|
||||
"category": "alarm",
|
||||
"description": "Emergency siren alarm for heavy sleepers",
|
||||
"bundleName": null
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
@ -83,12 +43,6 @@
|
||||
"name": "Mechanical",
|
||||
"description": "Mechanical and electronic sounds",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "alarm",
|
||||
"name": "Alarm Sounds",
|
||||
"description": "Wake-up and notification alarm sounds",
|
||||
"bundleName": "AlarmSounds"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
76
TheNoiseClock/Services/AlarmSoundService.swift
Normal file
76
TheNoiseClock/Services/AlarmSoundService.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// AlarmSoundService.swift
|
||||
// TheNoiseClock
|
||||
//
|
||||
// Created by Matt Bruce on 9/8/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// Extension service for alarm-specific sound functionality
|
||||
class AlarmSoundService {
|
||||
static let shared = AlarmSoundService()
|
||||
|
||||
// MARK: - Constants
|
||||
/// The category ID for alarm sounds as defined in alarm-sounds.json
|
||||
static let alarmCategoryId = "alarm"
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Load alarm sound configuration from alarm-sounds.json
|
||||
private func loadAlarmConfiguration() -> SoundConfiguration {
|
||||
guard let url = Bundle.main.url(forResource: "alarm-sounds", withExtension: "json") else {
|
||||
fatalError("❌ alarm-sounds.json not found in bundle. Ensure the file exists in your app bundle.")
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let config = try JSONDecoder().decode(SoundConfiguration.self, from: data)
|
||||
return config
|
||||
} catch {
|
||||
fatalError("❌ Error loading alarm sound configuration: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all available alarm sounds
|
||||
func getAlarmSounds() -> [Sound] {
|
||||
return loadAlarmConfiguration().sounds
|
||||
}
|
||||
|
||||
/// Get alarm sounds by category
|
||||
func getAlarmSoundsByCategory(_ categoryId: String) -> [Sound] {
|
||||
return loadAlarmConfiguration().sounds
|
||||
.filter { $0.category == categoryId }
|
||||
}
|
||||
|
||||
/// Get alarm sounds for the default alarm category
|
||||
func getAlarmSoundsForDefaultCategory() -> [Sound] {
|
||||
return getAlarmSoundsByCategory(Self.alarmCategoryId)
|
||||
}
|
||||
|
||||
/// Get default alarm sound
|
||||
func getDefaultAlarmSound() -> Sound? {
|
||||
let sounds = loadAlarmConfiguration().sounds
|
||||
return sounds.first { $0.isDefault == true } ?? sounds.first
|
||||
}
|
||||
|
||||
/// Get alarm sound categories
|
||||
func getAlarmSoundCategories() -> [SoundCategory] {
|
||||
return loadAlarmConfiguration().categories
|
||||
}
|
||||
|
||||
/// Get alarm sound settings
|
||||
func getAlarmSoundSettings() -> AudioSettings {
|
||||
return loadAlarmConfiguration().settings
|
||||
}
|
||||
|
||||
/// Get sound display name by filename
|
||||
func getSoundDisplayName(_ fileName: String) -> String {
|
||||
let alarmSounds = getAlarmSounds()
|
||||
if let sound = alarmSounds.first(where: { $0.fileName == fileName }) {
|
||||
return sound.name
|
||||
}
|
||||
return fileName.replacingOccurrences(of: ".caf", with: "").capitalized
|
||||
}
|
||||
}
|
||||
@ -125,10 +125,6 @@ struct AddAlarmView: View {
|
||||
|
||||
// MARK: - Helper Methods
|
||||
private func getSoundDisplayName(_ fileName: String) -> String {
|
||||
let alarmSounds = SoundConfigurationService.shared.getAlarmSounds()
|
||||
if let sound = alarmSounds.first(where: { $0.fileName == fileName }) {
|
||||
return sound.name
|
||||
}
|
||||
return fileName.replacingOccurrences(of: ".mp3", with: "").capitalized
|
||||
return AlarmSoundService.shared.getSoundDisplayName(fileName)
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@ struct SoundSelectionView: View {
|
||||
|
||||
// Use shared player instance to avoid audio conflicts
|
||||
private let noisePlayer = NoisePlayer.shared
|
||||
private let alarmSounds = SoundConfigurationService.shared.getAlarmSounds().sorted { $0.name < $1.name }
|
||||
private let alarmSounds = AlarmSoundService.shared.getAlarmSounds().sorted { $0.name < $1.name }
|
||||
|
||||
@State private var isPlaying = false
|
||||
@State private var currentlyPlayingSound: String? = nil
|
||||
|
||||
@ -145,11 +145,7 @@ struct EditAlarmView: View {
|
||||
|
||||
// MARK: - Helper Methods
|
||||
private func getSoundDisplayName(_ fileName: String) -> String {
|
||||
let alarmSounds = SoundConfigurationService.shared.getAlarmSounds()
|
||||
if let sound = alarmSounds.first(where: { $0.fileName == fileName }) {
|
||||
return sound.name
|
||||
}
|
||||
return fileName.replacingOccurrences(of: ".mp3", with: "").capitalized
|
||||
return AlarmSoundService.shared.getSoundDisplayName(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user