Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-09-09 09:57:27 -05:00
parent 706aba5d30
commit da7214a8f9
16 changed files with 309 additions and 234 deletions

View File

@ -10,32 +10,25 @@ import Foundation
/// Configuration model for sound system loaded from JSON /// Configuration model for sound system loaded from JSON
public struct SoundConfiguration: Codable { public struct SoundConfiguration: Codable {
public let sounds: [Sound] public let sounds: [Sound]
public let categories: [SoundCategory]?
public let settings: AudioSettings public let settings: AudioSettings
public init(sounds: [Sound], categories: [SoundCategory]? = nil, settings: AudioSettings) { public init(sounds: [Sound], settings: AudioSettings) {
self.sounds = sounds self.sounds = sounds
self.categories = categories
self.settings = settings self.settings = settings
} }
} }
/// Simple struct for loading just the sounds array from category JSON files
/// Sound category configuration public struct SoundsOnly: Codable {
public struct SoundCategory: Codable, Identifiable { public let sounds: [Sound]
public let id: String
public let name: String
public let description: String
public let bundleName: String? // Optional bundle name for this category
public init(id: String, name: String, description: String, bundleName: String? = nil) { public init(sounds: [Sound]) {
self.id = id self.sounds = sounds
self.name = name
self.description = description
self.bundleName = bundleName
} }
} }
/// Audio settings configuration /// Audio settings configuration
public struct AudioSettings: Codable { public struct AudioSettings: Codable {
public let defaultVolume: Float public let defaultVolume: Float
@ -65,27 +58,78 @@ public class SoundConfigurationService {
private init() {} private init() {}
/// Load sound configuration from JSON file
public func loadConfiguration(from bundle: Bundle = .main, fileName: String = "sounds") -> SoundConfiguration { /// Load audio settings from SoundsSettings.json
guard let url = bundle.url(forResource: fileName, withExtension: "json") else { private func loadAudioSettings(from bundle: Bundle = .main) -> AudioSettings {
fatalError("\(fileName).json not found in bundle. Ensure the file exists in your app bundle.") guard let url = bundle.url(forResource: "SoundsSettings", withExtension: "json") else {
print("⚠️ Warning: SoundsSettings.json not found, using default settings")
return AudioSettings(
defaultVolume: 0.8,
defaultLoopCount: -1,
preloadSounds: true,
preloadStrategy: "category",
audioSessionCategory: "playback",
audioSessionMode: "default",
audioSessionOptions: ["mixWithOthers"]
)
} }
do { do {
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
let config = try JSONDecoder().decode(SoundConfiguration.self, from: data) let settings = try JSONDecoder().decode(AudioSettings.self, from: data)
self.configuration = config print("✅ Loaded audio settings from SoundsSettings.json")
print("✅ Loaded sound configuration with \(config.sounds.count) sounds") return settings
return config
} catch { } catch {
fatalError("❌ Error loading sound configuration: \(error)") print("⚠️ Warning: Error loading audio settings, using defaults: \(error)")
return AudioSettings(
defaultVolume: 0.8,
defaultLoopCount: -1,
preloadSounds: true,
preloadStrategy: "category",
audioSessionCategory: "playback",
audioSessionMode: "default",
audioSessionOptions: ["mixWithOthers"]
)
} }
} }
/// Load sound configuration from multiple category-specific JSON files
public func loadConfigurationFromBundles(from bundle: Bundle = .main) -> SoundConfiguration {
let bundleNames = ["Colored", "Nature", "Mechanical", "Ambient"]
var allSounds: [Sound] = []
for bundleName in bundleNames {
guard let bundleURL = bundle.url(forResource: bundleName, withExtension: "bundle"),
let categoryBundle = Bundle(url: bundleURL),
let url = categoryBundle.url(forResource: "sounds", withExtension: "json") else {
print("⚠️ Warning: Could not find sounds.json in \(bundleName).bundle")
continue
}
do {
let data = try Data(contentsOf: url)
let soundsOnly = try JSONDecoder().decode(SoundsOnly.self, from: data)
allSounds.append(contentsOf: soundsOnly.sounds)
print("✅ Loaded \(soundsOnly.sounds.count) sounds from \(bundleName).bundle")
} catch {
print("⚠️ Warning: Error loading sounds from \(bundleName).bundle: \(error)")
}
}
// Load settings from separate file
let settings = loadAudioSettings(from: bundle)
let config = SoundConfiguration(sounds: allSounds, settings: settings)
self.configuration = config
print("✅ Loaded combined sound configuration with \(allSounds.count) sounds from \(bundleNames.count) bundles")
return config
}
/// Get current configuration /// Get current configuration
public func getConfiguration() -> SoundConfiguration { public func getConfiguration() -> SoundConfiguration {
if configuration == nil { if configuration == nil {
return loadConfiguration() return loadConfigurationFromBundles()
} }
return configuration! return configuration!
} }
@ -101,16 +145,6 @@ public class SoundConfigurationService {
.filter { $0.category == categoryId } .filter { $0.category == categoryId }
} }
/// Get sounds by bundle name
public func getSoundsByBundle(_ bundleName: String) -> [Sound] {
return getConfiguration().sounds
.filter { $0.bundleName == bundleName }
}
/// Get available categories
public func getAvailableCategories() -> [SoundCategory] {
return getConfiguration().categories ?? []
}
/// Get audio settings /// Get audio settings
public func getAudioSettings() -> AudioSettings { public func getAudioSettings() -> AudioSettings {

View File

@ -59,7 +59,7 @@ enum AppConstants {
// MARK: - System Sounds // MARK: - System Sounds
enum SystemSounds { enum SystemSounds {
static let defaultSound = "default" static let defaultSound = "digital-alarm.caf"
static let availableSounds = ["default", "bell", "chimes", "ding", "glass", "silence"] static let availableSounds = ["default", "bell", "chimes", "ding", "glass", "silence"]
} }
} }

View File

@ -36,7 +36,7 @@ enum NotificationUtils {
content.title = title content.title = title
content.body = body content.body = body
if soundName == AppConstants.SystemSounds.defaultSound { if soundName == "default" {
content.sound = UNNotificationSound.default content.sound = UNNotificationSound.default
print("🔔 Using default notification sound") print("🔔 Using default notification sound")
} else { } else {

View File

@ -41,17 +41,5 @@
"category": "alarm", "category": "alarm",
"bundleName": null "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
}
} }

View File

@ -0,0 +1,46 @@
{
"sounds": [
{
"id": "ambient-pad",
"name": "Atmospheric Pad",
"fileName": "atmospheric-pad.mp3",
"category": "ambient",
"description": "Soothing atmospheric drone for meditation or focus",
"bundleName": "Ambient",
"sourceUrl": "https://pixabay.com/sound-effects/search/ambient%20drone/"
},
{
"id": "calm-pad",
"name": "Calm Ambient Pad",
"fileName": "calm-ambient-pad.mp3",
"category": "ambient",
"description": "Soft, warm ambient pad for deep relaxation",
"bundleName": "Ambient",
"sourceUrl": "https://pixabay.com/sound-effects/search/ambient%20pad/"
},
{
"id": "dark-ambient",
"name": "Dark Ambient Atmosphere",
"fileName": "dark-ambient.mp3",
"category": "ambient",
"description": "A moody, atmospheric soundscape with deep tones, ideal for introspection or creative focus.",
"bundleName": "Ambient"
},
{
"id": "ethereal-ambient",
"name": "Ethereal Ambient Soundscape",
"fileName": "ethereal-ambient.mp3",
"category": "ambient",
"description": "A dreamy, ethereal sound with delicate tones, perfect for relaxation or spiritual practices.",
"bundleName": "Ambient"
},
{
"id": "ambient-waves",
"name": "Ambient Waves",
"fileName": "ambient-waves.mp3",
"category": "ambient",
"description": "A smooth, wave-like ambient sound, evoking a sense of calm flow. Ideal for meditation or focus.",
"bundleName": "Ambient"
}
]
}

View File

@ -0,0 +1,45 @@
{
"sounds": [
{
"id": "white-noise",
"name": "White Noise",
"fileName": "white-noise.mp3",
"category": "colored",
"description": "Classic white noise with equal energy across frequencies for focus and relaxation",
"bundleName": "Colored"
},
{
"id": "pink-noise",
"name": "Pink Noise",
"fileName": "pink-noise.mp3",
"category": "colored",
"description": "Soft, warm noise resembling steady rain, ideal for relaxation",
"bundleName": "Colored",
"sourceUrl": "https://freesound.org/search/?q=pink+noise+loop"
},
{
"id": "brown-noise",
"name": "Brown Noise",
"fileName": "brown-noise.mp3",
"category": "colored",
"description": "Deep, rumbling noise like distant thunder, great for deep sleep",
"bundleName": "Colored"
},
{
"id": "green-noise",
"name": "Green Noise",
"fileName": "green-noise.mp3",
"category": "colored",
"description": "Mid-range noise resembling rustling leaves, soothing and nature-like",
"bundleName": "Colored"
},
{
"id": "grey-noise",
"name": "Grey Noise",
"fileName": "grey-noise.mp3",
"category": "colored",
"description": "Balanced noise adjusted for human hearing, perfect for calm focus",
"bundleName": "Colored"
}
]
}

View File

@ -0,0 +1,44 @@
{
"sounds": [
{
"id": "fan-noise",
"name": "Fan Heater",
"fileName": "fan-heater.mp3",
"category": "mechanical",
"description": "Fan and heater sounds for consistent background noise",
"bundleName": "Mechanical"
},
{
"id": "air-conditioner",
"name": "Air Conditioner Hum",
"fileName": "air-conditioner-hum.mp3",
"category": "mechanical",
"description": "Steady hum of an air conditioner for focus or sleep",
"bundleName": "Mechanical"
},
{
"id": "clock-ticking",
"name": "Clock Ticking Mechanism",
"fileName": "clock-ticking.mp3",
"category": "mechanical",
"description": "The rhythmic ticking of a clock, providing a consistent, hypnotic sound for focus or relaxation.",
"bundleName": "Mechanical"
},
{
"id": "electric-fan",
"name": "Electric Fan Whirring",
"fileName": "electric-fan.mp3",
"category": "mechanical",
"description": "The steady whir of an electric fan, creating a monotonous sound for sleep or concentration.",
"bundleName": "Mechanical"
},
{
"id": "engine-idling",
"name": "Engine Idling",
"fileName": "engine-idling.mp3",
"category": "mechanical",
"description": "A low, steady engine idle, offering a deep hum for background noise or relaxation.",
"bundleName": "Mechanical"
}
]
}

View File

@ -0,0 +1,44 @@
{
"sounds": [
{
"id": "heavy-rain",
"name": "Heavy Rain",
"fileName": "heavy-rain.mp3",
"category": "nature",
"description": "Heavy rainfall sounds for peaceful sleep",
"bundleName": "Nature"
},
{
"id": "ocean-waves",
"name": "Ocean Waves",
"fileName": "ocean-waves.mp3",
"category": "nature",
"description": "Gentle waves crashing on the shore for relaxation",
"bundleName": "Nature"
},
{
"id": "forest-ambience",
"name": "Forest Ambience",
"fileName": "forest-ambience.mp3",
"category": "nature",
"description": "Calm forest sounds with birds and wind for nature lovers",
"bundleName": "Nature"
},
{
"id": "crickets-night",
"name": "Crickets at Night",
"fileName": "crickets-night.mp3",
"category": "nature",
"description": "The rhythmic chirping of crickets under a night sky, perfect for calming sleep.",
"bundleName": "Nature"
},
{
"id": "distant-thunderstorm",
"name": "Distant Thunderstorm",
"fileName": "distant-thunderstorm.mp3",
"category": "nature",
"description": "Soft thunder and rain in the distance, creating a soothing, stormy ambiance.",
"bundleName": "Nature"
}
]
}

View File

@ -0,0 +1,9 @@
{
"defaultVolume": 0.8,
"defaultLoopCount": -1,
"preloadSounds": true,
"preloadStrategy": "category",
"audioSessionCategory": "playback",
"audioSessionMode": "default",
"audioSessionOptions": ["mixWithOthers"]
}

View File

@ -1,176 +0,0 @@
{
"sounds": [
{
"id": "white-noise",
"name": "White Noise",
"fileName": "white-noise.mp3",
"category": "colored",
"description": "Classic white noise with equal energy across frequencies for focus and relaxation",
"bundleName": "Colored"
},
{
"id": "pink-noise",
"name": "Pink Noise",
"fileName": "pink-noise.mp3",
"category": "colored",
"description": "Soft, warm noise resembling steady rain, ideal for relaxation",
"bundleName": "Colored",
"sourceUrl": "https://freesound.org/search/?q=pink+noise+loop"
},
{
"id": "brown-noise",
"name": "Brown Noise",
"fileName": "brown-noise.mp3",
"category": "colored",
"description": "Deep, rumbling noise like distant thunder, great for deep sleep",
"bundleName": "Colored"
},
{
"id": "green-noise",
"name": "Green Noise",
"fileName": "green-noise.mp3",
"category": "colored",
"description": "Mid-range noise resembling rustling leaves, soothing and nature-like",
"bundleName": "Colored"
},
{
"id": "grey-noise",
"name": "Grey Noise",
"fileName": "grey-noise.mp3",
"category": "colored",
"description": "Balanced noise adjusted for human hearing, perfect for calm focus",
"bundleName": "Colored"
},
{
"id": "heavy-rain",
"name": "Heavy Rain",
"fileName": "heavy-rain.mp3",
"category": "nature",
"description": "Heavy rainfall sounds for peaceful sleep",
"bundleName": "Nature"
},
{
"id": "ocean-waves",
"name": "Ocean Waves",
"fileName": "ocean-waves.mp3",
"category": "nature",
"description": "Gentle waves crashing on the shore for relaxation",
"bundleName": "Nature"
},
{
"id": "forest-ambience",
"name": "Forest Ambience",
"fileName": "forest-ambience.mp3",
"category": "nature",
"description": "Calm forest sounds with birds and wind for nature lovers",
"bundleName": "Nature"
},
{
"id": "fan-noise",
"name": "Fan Heater",
"fileName": "fan-heater.mp3",
"category": "mechanical",
"description": "Fan and heater sounds for consistent background noise",
"bundleName": "Mechanical"
},
{
"id": "air-conditioner",
"name": "Air Conditioner Hum",
"fileName": "air-conditioner-hum.mp3",
"category": "mechanical",
"description": "Steady hum of an air conditioner for focus or sleep",
"bundleName": "Mechanical"
},
{
"id": "ambient-pad",
"name": "Atmospheric Pad",
"fileName": "atmospheric-pad.mp3",
"category": "ambient",
"description": "Soothing atmospheric drone for meditation or focus",
"bundleName": "Ambient",
"sourceUrl": "https://pixabay.com/sound-effects/search/ambient%20drone/"
},
{
"id": "calm-pad",
"name": "Calm Ambient Pad",
"fileName": "calm-ambient-pad.mp3",
"category": "ambient",
"description": "Soft, warm ambient pad for deep relaxation",
"bundleName": "Ambient",
"sourceUrl": "https://pixabay.com/sound-effects/search/ambient%20pad/"
},
{
"id": "dark-ambient",
"name": "Dark Ambient Atmosphere",
"fileName": "dark-ambient.mp3",
"category": "ambient",
"description": "A moody, atmospheric soundscape with deep tones, ideal for introspection or creative focus.",
"bundleName": "Ambient"
},
{
"id": "ethereal-ambient",
"name": "Ethereal Ambient Soundscape",
"fileName": "ethereal-ambient.mp3",
"category": "ambient",
"description": "A dreamy, ethereal sound with delicate tones, perfect for relaxation or spiritual practices.",
"bundleName": "Ambient"
},
{
"id": "ambient-waves",
"name": "Ambient Waves",
"fileName": "ambient-waves.mp3",
"category": "ambient",
"description": "A smooth, wave-like ambient sound, evoking a sense of calm flow. Ideal for meditation or focus.",
"bundleName": "Ambient"
},
{
"id": "clock-ticking",
"name": "Clock Ticking Mechanism",
"fileName": "clock-ticking.mp3",
"category": "mechanical",
"description": "The rhythmic ticking of a clock, providing a consistent, hypnotic sound for focus or relaxation.",
"bundleName": "Mechanical"
},
{
"id": "electric-fan",
"name": "Electric Fan Whirring",
"fileName": "electric-fan.mp3",
"category": "mechanical",
"description": "The steady whir of an electric fan, creating a monotonous sound for sleep or concentration.",
"bundleName": "Mechanical"
},
{
"id": "engine-idling",
"name": "Engine Idling",
"fileName": "engine-idling.mp3",
"category": "mechanical",
"description": "A low, steady engine idle, offering a deep hum for background noise or relaxation.",
"bundleName": "Mechanical"
},
{
"id": "crickets-night",
"name": "Crickets at Night",
"fileName": "crickets-night.mp3",
"category": "nature",
"description": "The rhythmic chirping of crickets under a night sky, perfect for calming sleep.",
"bundleName": "Nature"
},
{
"id": "distant-thunderstorm",
"name": "Distant Thunderstorm",
"fileName": "distant-thunderstorm.mp3",
"category": "nature",
"description": "Soft thunder and rain in the distance, creating a soothing, stormy ambiance.",
"bundleName": "Nature"
}
],
"settings": {
"defaultVolume": 0.8,
"defaultLoopCount": -1,
"preloadSounds": true,
"preloadStrategy": "category",
"audioSessionCategory": "playback",
"audioSessionMode": "default",
"audioSessionOptions": ["mixWithOthers"]
}
}

View File

@ -18,21 +18,61 @@ class AlarmSoundService {
private init() {} private init() {}
/// Load alarm sound configuration from alarm-sounds.json /// Load alarm sound configuration from AlarmSounds.bundle
private func loadAlarmConfiguration() -> SoundConfiguration { private func loadAlarmConfiguration() -> SoundConfiguration {
guard let url = Bundle.main.url(forResource: "alarm-sounds", withExtension: "json") else { guard let bundleURL = Bundle.main.url(forResource: "AlarmSounds", withExtension: "bundle"),
fatalError("❌ alarm-sounds.json not found in bundle. Ensure the file exists in your app bundle.") let alarmBundle = Bundle(url: bundleURL),
let url = alarmBundle.url(forResource: "sounds", withExtension: "json") else {
fatalError("❌ sounds.json not found in AlarmSounds.bundle. Ensure the bundle and file exist.")
} }
do { do {
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
let config = try JSONDecoder().decode(SoundConfiguration.self, from: data) let soundsOnly = try JSONDecoder().decode(SoundsOnly.self, from: data)
return config
// Load settings from separate SoundsSettings.json file
let settings = loadAudioSettings()
return SoundConfiguration(sounds: soundsOnly.sounds, settings: settings)
} catch { } catch {
fatalError("❌ Error loading alarm sound configuration: \(error)") fatalError("❌ Error loading alarm sound configuration: \(error)")
} }
} }
/// Load audio settings from SoundsSettings.json
private func loadAudioSettings() -> AudioSettings {
guard let url = Bundle.main.url(forResource: "SoundsSettings", withExtension: "json") else {
print("⚠️ Warning: SoundsSettings.json not found, using default alarm settings")
return AudioSettings(
defaultVolume: 1.0,
defaultLoopCount: -1,
preloadSounds: true,
preloadStrategy: "category",
audioSessionCategory: "playback",
audioSessionMode: "default",
audioSessionOptions: ["mixWithOthers"]
)
}
do {
let data = try Data(contentsOf: url)
let settings = try JSONDecoder().decode(AudioSettings.self, from: data)
print("✅ Loaded audio settings for alarms from SoundsSettings.json")
return settings
} catch {
print("⚠️ Warning: Error loading audio settings for alarms, using defaults: \(error)")
return AudioSettings(
defaultVolume: 1.0,
defaultLoopCount: -1,
preloadSounds: true,
preloadStrategy: "category",
audioSessionCategory: "playback",
audioSessionMode: "default",
audioSessionOptions: ["mixWithOthers"]
)
}
}
/// Get all available alarm sounds /// Get all available alarm sounds
func getAlarmSounds() -> [Sound] { func getAlarmSounds() -> [Sound] {
return loadAlarmConfiguration().sounds return loadAlarmConfiguration().sounds

View File

@ -101,7 +101,7 @@ class FocusModeService {
content.title = title content.title = title
content.body = body content.body = body
// Use the sound name directly since sounds.json now references CAF files // Use the sound name directly since sounds.json now references CAF files
if soundName == AppConstants.SystemSounds.defaultSound { if soundName == "default" {
content.sound = UNNotificationSound.default content.sound = UNNotificationSound.default
print("🔔 Using default notification sound") print("🔔 Using default notification sound")
} else { } else {

View File

@ -16,7 +16,7 @@ struct AddAlarmView: View {
@Binding var isPresented: Bool @Binding var isPresented: Bool
@State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date() @State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date()
@State private var selectedSoundName = "digital-alarm.mp3" @State private var selectedSoundName = "digital-alarm.caf"
@State private var alarmLabel = "Alarm" @State private var alarmLabel = "Alarm"
@State private var notificationMessage = "Your alarm is ringing" @State private var notificationMessage = "Your alarm is ringing"
@State private var snoozeDuration = 9 // minutes @State private var snoozeDuration = 9 // minutes

View File

@ -27,7 +27,7 @@ struct AlarmRowView: View {
.font(.subheadline) .font(.subheadline)
.foregroundColor(UIConstants.Colors.secondaryText) .foregroundColor(UIConstants.Colors.secondaryText)
Text("\(alarm.soundName)") Text("\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
.font(.caption) .font(.caption)
.foregroundColor(UIConstants.Colors.secondaryText) .foregroundColor(UIConstants.Colors.secondaryText)
} }

View File

@ -126,6 +126,7 @@ struct SoundCategoryView: View {
isPlaying: viewModel.isPlaying && selectedSound?.id == sound.id, isPlaying: viewModel.isPlaying && selectedSound?.id == sound.id,
isPreviewing: viewModel.isPreviewing && viewModel.previewSound?.id == sound.id, isPreviewing: viewModel.isPreviewing && viewModel.previewSound?.id == sound.id,
onSelect: { onSelect: {
viewModel.selectSound(sound)
selectedSound = sound selectedSound = sound
}, },
onPreview: { onPreview: {