diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift index 0eeb554..3269e98 100644 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift @@ -10,10 +10,10 @@ import Foundation /// Configuration model for sound system loaded from JSON public struct SoundConfiguration: Codable { public let sounds: [Sound] - public let categories: [SoundCategory] + public let categories: [SoundCategory]? public let settings: AudioSettings - public init(sounds: [Sound], categories: [SoundCategory], settings: AudioSettings) { + public init(sounds: [Sound], categories: [SoundCategory]? = nil, settings: AudioSettings) { self.sounds = sounds self.categories = categories self.settings = settings @@ -109,7 +109,7 @@ public class SoundConfigurationService { /// Get available categories public func getAvailableCategories() -> [SoundCategory] { - return getConfiguration().categories + return getConfiguration().categories ?? [] } /// Get audio settings diff --git a/TheNoiseClock/Models/SoundCategory.swift b/TheNoiseClock/Models/SoundCategory.swift new file mode 100644 index 0000000..506bf77 --- /dev/null +++ b/TheNoiseClock/Models/SoundCategory.swift @@ -0,0 +1,100 @@ +// +// SoundCategory.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/8/25. +// + +import Foundation + +/// Enum representing sound categories with associated icons, display names, and metadata +public enum SoundCategory: String, CaseIterable, Identifiable { + case all = "all" + case colored = "colored" + case ambient = "ambient" + case nature = "nature" + case mechanical = "mechanical" + case alarm = "alarm" + + public var id: String { rawValue } + + /// Display name for the category + public var displayName: String { + switch self { + case .all: return "All" + case .colored: return "Colored" + case .ambient: return "Ambient" + case .nature: return "Nature" + case .mechanical: return "Mechanical" + case .alarm: return "Alarm Sounds" + } + } + + /// SF Symbol icon name for the category + public var icon: String { + switch self { + case .all: return "speaker.wave.2" + case .colored: return "waveform.path" + case .ambient: return "waveform" + case .nature: return "cloud.rain" + case .mechanical: return "fan" + case .alarm: return "alarm" + } + } + + /// Bundle name for organizing audio files + public var bundleName: String? { + switch self { + case .all: return nil + case .colored: return "Colored" + case .ambient: return "Ambient" + case .nature: return "Nature" + case .mechanical: return "Mechanical" + case .alarm: return nil + } + } + + /// Description of the category + public var description: String { + switch self { + case .all: return "All available sounds" + case .colored: return "Synthetic noise signals for focus, sleep, and relaxation" + case .ambient: return "General ambient sounds" + case .nature: return "Natural environmental sounds" + case .mechanical: return "Mechanical and electronic sounds" + case .alarm: return "Wake-up and notification alarm sounds" + } + } + + /// Preferred sort order for categories (lower number = appears first) + public var sortOrder: Int { + switch self { + case .all: return 0 + case .colored: return 1 + case .ambient: return 2 + case .nature: return 3 + case .mechanical: return 4 + case .alarm: return 5 + } + } + + /// Initialize from string, returning nil if invalid + public init?(from string: String) { + self.init(rawValue: string) + } + + /// Get all non-alarm categories for noise selection + public static var noiseCategories: [SoundCategory] { + return [.all, .colored, .ambient, .nature, .mechanical] + } + + /// Get all categories sorted by preferred order + public static var sortedCategories: [SoundCategory] { + return allCases.sorted { $0.sortOrder < $1.sortOrder } + } + + /// Get noise categories sorted by preferred order + public static var sortedNoiseCategories: [SoundCategory] { + return noiseCategories.sorted { $0.sortOrder < $1.sortOrder } + } +} diff --git a/TheNoiseClock/Resources/Ambient.bundle/ambient-waves.mp3 b/TheNoiseClock/Resources/Ambient.bundle/ambient-waves.mp3 new file mode 100644 index 0000000..d54cff7 Binary files /dev/null and b/TheNoiseClock/Resources/Ambient.bundle/ambient-waves.mp3 differ diff --git a/TheNoiseClock/Resources/Ambient.bundle/atmospheric-pad.mp3 b/TheNoiseClock/Resources/Ambient.bundle/atmospheric-pad.mp3 new file mode 100644 index 0000000..e1f86ed Binary files /dev/null and b/TheNoiseClock/Resources/Ambient.bundle/atmospheric-pad.mp3 differ diff --git a/TheNoiseClock/Resources/Ambient.bundle/calm-ambient-pad.mp3 b/TheNoiseClock/Resources/Ambient.bundle/calm-ambient-pad.mp3 new file mode 100644 index 0000000..bc2e3cf Binary files /dev/null and b/TheNoiseClock/Resources/Ambient.bundle/calm-ambient-pad.mp3 differ diff --git a/TheNoiseClock/Resources/Ambient.bundle/dark-ambient.mp3 b/TheNoiseClock/Resources/Ambient.bundle/dark-ambient.mp3 new file mode 100644 index 0000000..03d4227 Binary files /dev/null and b/TheNoiseClock/Resources/Ambient.bundle/dark-ambient.mp3 differ diff --git a/TheNoiseClock/Resources/Ambient.bundle/ethereal-ambient.mp3 b/TheNoiseClock/Resources/Ambient.bundle/ethereal-ambient.mp3 new file mode 100644 index 0000000..8fa682d Binary files /dev/null and b/TheNoiseClock/Resources/Ambient.bundle/ethereal-ambient.mp3 differ diff --git a/TheNoiseClock/Resources/Colored.bundle/brown-noise.mp3 b/TheNoiseClock/Resources/Colored.bundle/brown-noise.mp3 new file mode 100644 index 0000000..439ca0e Binary files /dev/null and b/TheNoiseClock/Resources/Colored.bundle/brown-noise.mp3 differ diff --git a/TheNoiseClock/Resources/Colored.bundle/green-noise.mp3 b/TheNoiseClock/Resources/Colored.bundle/green-noise.mp3 new file mode 100644 index 0000000..102d83c Binary files /dev/null and b/TheNoiseClock/Resources/Colored.bundle/green-noise.mp3 differ diff --git a/TheNoiseClock/Resources/Colored.bundle/grey-noise.mp3 b/TheNoiseClock/Resources/Colored.bundle/grey-noise.mp3 new file mode 100644 index 0000000..4c6b4dd Binary files /dev/null and b/TheNoiseClock/Resources/Colored.bundle/grey-noise.mp3 differ diff --git a/TheNoiseClock/Resources/Colored.bundle/pink-noise.mp3 b/TheNoiseClock/Resources/Colored.bundle/pink-noise.mp3 new file mode 100644 index 0000000..140ede5 Binary files /dev/null and b/TheNoiseClock/Resources/Colored.bundle/pink-noise.mp3 differ diff --git a/TheNoiseClock/Resources/Ambient.bundle/white-noise.mp3 b/TheNoiseClock/Resources/Colored.bundle/white-noise.mp3 similarity index 100% rename from TheNoiseClock/Resources/Ambient.bundle/white-noise.mp3 rename to TheNoiseClock/Resources/Colored.bundle/white-noise.mp3 diff --git a/TheNoiseClock/Resources/Mechanical.bundle/air-conditioner-hum.mp3 b/TheNoiseClock/Resources/Mechanical.bundle/air-conditioner-hum.mp3 new file mode 100644 index 0000000..30d72eb Binary files /dev/null and b/TheNoiseClock/Resources/Mechanical.bundle/air-conditioner-hum.mp3 differ diff --git a/TheNoiseClock/Resources/Mechanical.bundle/clock-ticking.mp3 b/TheNoiseClock/Resources/Mechanical.bundle/clock-ticking.mp3 new file mode 100644 index 0000000..446ed43 Binary files /dev/null and b/TheNoiseClock/Resources/Mechanical.bundle/clock-ticking.mp3 differ diff --git a/TheNoiseClock/Resources/Mechanical.bundle/electric-fan.mp3 b/TheNoiseClock/Resources/Mechanical.bundle/electric-fan.mp3 new file mode 100644 index 0000000..ec5e686 Binary files /dev/null and b/TheNoiseClock/Resources/Mechanical.bundle/electric-fan.mp3 differ diff --git a/TheNoiseClock/Resources/Mechanical.bundle/engine-idling.mp3 b/TheNoiseClock/Resources/Mechanical.bundle/engine-idling.mp3 new file mode 100644 index 0000000..ca2ecda Binary files /dev/null and b/TheNoiseClock/Resources/Mechanical.bundle/engine-idling.mp3 differ diff --git a/TheNoiseClock/Resources/Mechanical.bundle/fan-white-noise-heater.mp3 b/TheNoiseClock/Resources/Mechanical.bundle/fan-heater.mp3 similarity index 100% rename from TheNoiseClock/Resources/Mechanical.bundle/fan-white-noise-heater.mp3 rename to TheNoiseClock/Resources/Mechanical.bundle/fan-heater.mp3 diff --git a/TheNoiseClock/Resources/Nature.bundle/crickets-night.mp3 b/TheNoiseClock/Resources/Nature.bundle/crickets-night.mp3 new file mode 100644 index 0000000..3778cb4 Binary files /dev/null and b/TheNoiseClock/Resources/Nature.bundle/crickets-night.mp3 differ diff --git a/TheNoiseClock/Resources/Nature.bundle/distant-thunderstorm.mp3 b/TheNoiseClock/Resources/Nature.bundle/distant-thunderstorm.mp3 new file mode 100644 index 0000000..eb583f4 Binary files /dev/null and b/TheNoiseClock/Resources/Nature.bundle/distant-thunderstorm.mp3 differ diff --git a/TheNoiseClock/Resources/Nature.bundle/forest-ambience.mp3 b/TheNoiseClock/Resources/Nature.bundle/forest-ambience.mp3 new file mode 100644 index 0000000..6196300 Binary files /dev/null and b/TheNoiseClock/Resources/Nature.bundle/forest-ambience.mp3 differ diff --git a/TheNoiseClock/Resources/Nature.bundle/heavy-rain-white-noise.mp3 b/TheNoiseClock/Resources/Nature.bundle/heavy-rain.mp3 similarity index 100% rename from TheNoiseClock/Resources/Nature.bundle/heavy-rain-white-noise.mp3 rename to TheNoiseClock/Resources/Nature.bundle/heavy-rain.mp3 diff --git a/TheNoiseClock/Resources/Nature.bundle/ocean-waves.mp3 b/TheNoiseClock/Resources/Nature.bundle/ocean-waves.mp3 new file mode 100644 index 0000000..1a45632 Binary files /dev/null and b/TheNoiseClock/Resources/Nature.bundle/ocean-waves.mp3 differ diff --git a/TheNoiseClock/Resources/alarm-sounds.json b/TheNoiseClock/Resources/alarm-sounds.json index 6c0bc11..e8157e8 100644 --- a/TheNoiseClock/Resources/alarm-sounds.json +++ b/TheNoiseClock/Resources/alarm-sounds.json @@ -42,14 +42,6 @@ "bundleName": null } ], - "categories": [ - { - "id": "alarm", - "name": "Alarm Sounds", - "description": "Wake-up and notification alarm sounds", - "bundleName": null - } - ], "settings": { "defaultVolume": 1.0, "defaultLoopCount": -1, diff --git a/TheNoiseClock/Resources/sounds.json b/TheNoiseClock/Resources/sounds.json index e10dddd..ed46118 100644 --- a/TheNoiseClock/Resources/sounds.json +++ b/TheNoiseClock/Resources/sounds.json @@ -4,45 +4,164 @@ "id": "white-noise", "name": "White Noise", "fileName": "white-noise.mp3", - "category": "ambient", - "description": "Classic white noise for focus and relaxation", - "bundleName": "Ambient" + "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 White Noise", - "fileName": "heavy-rain-white-noise.mp3", + "name": "Heavy Rain", + "fileName": "heavy-rain.mp3", "category": "nature", "description": "Heavy rainfall sounds for peaceful sleep", "bundleName": "Nature" }, { - "id": "fan-noise", - "name": "Fan White Noise", - "fileName": "fan-white-noise-heater.mp3", - "category": "mechanical", - "description": "Fan and heater sounds for consistent background noise", - "bundleName": "Mechanical" - } - ], - "categories": [ - { - "id": "ambient", - "name": "Ambient", - "description": "General ambient sounds", - "bundleName": "Ambient" - }, - { - "id": "nature", - "name": "Nature", - "description": "Natural environmental sounds", + "id": "ocean-waves", + "name": "Ocean Waves", + "fileName": "ocean-waves.mp3", + "category": "nature", + "description": "Gentle waves crashing on the shore for relaxation", "bundleName": "Nature" }, { - "id": "mechanical", - "name": "Mechanical", - "description": "Mechanical and electronic sounds", + "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": { diff --git a/TheNoiseClock/Services/AlarmSoundService.swift b/TheNoiseClock/Services/AlarmSoundService.swift index 5de9d3c..26485b6 100644 --- a/TheNoiseClock/Services/AlarmSoundService.swift +++ b/TheNoiseClock/Services/AlarmSoundService.swift @@ -56,8 +56,8 @@ class AlarmSoundService { } /// Get alarm sound categories - func getAlarmSoundCategories() -> [SoundCategory] { - return loadAlarmConfiguration().categories + func getAlarmSoundCategories() -> [TheNoiseClock.SoundCategory] { + return [.alarm] } /// Get alarm sound settings diff --git a/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift b/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift index fd561f6..95e04ec 100644 --- a/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift +++ b/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift @@ -14,56 +14,56 @@ struct SoundCategoryView: View { // MARK: - Properties let sounds: [Sound] @Binding var selectedSound: Sound? - @State private var selectedCategory: String = "all" + @State private var selectedCategory: SoundCategory = .all @State private var searchText: String = "" @State private var viewModel = SoundViewModel() // MARK: - Computed Properties private var filteredSounds: [Sound] { - let nonAlarmSounds = sounds.filter { $0.category != "alarm" } + let nonAlarmSounds = sounds.filter { $0.category != SoundCategory.alarm.rawValue } - let categoryFiltered = selectedCategory == "all" + let categoryFiltered = selectedCategory == .all ? nonAlarmSounds - : nonAlarmSounds.filter { $0.category == selectedCategory } + : nonAlarmSounds.filter { $0.category == selectedCategory.rawValue } - if searchText.isEmpty { - return categoryFiltered + let searchFiltered = if searchText.isEmpty { + categoryFiltered } else { - return categoryFiltered.filter { sound in + categoryFiltered.filter { sound in sound.name.localizedCaseInsensitiveContains(searchText) || sound.description.localizedCaseInsensitiveContains(searchText) } } + + // Sort sounds alphabetically by name + return searchFiltered.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } // MARK: - Helper Methods - private func getCategoryCount(for category: String) -> Int { - let nonAlarmSounds = sounds.filter { $0.category != "alarm" } + private func getCategoryCount(for category: SoundCategory) -> Int { + let nonAlarmSounds = sounds.filter { $0.category != SoundCategory.alarm.rawValue } - if category == "all" { + if category == .all { return nonAlarmSounds.count } else { - return nonAlarmSounds.filter { $0.category == category }.count + return nonAlarmSounds.filter { $0.category == category.rawValue }.count } } - private var categories: [String] { - let nonAlarmSounds = sounds.filter { $0.category != "alarm" } - let uniqueCategories = Set(nonAlarmSounds.map { $0.category }) - return ["all"] + Array(uniqueCategories).sorted() + private var categories: [SoundCategory] { + let nonAlarmSounds = sounds.filter { $0.category != SoundCategory.alarm.rawValue } + let uniqueCategoryStrings = Set(nonAlarmSounds.map { $0.category }) + + // Convert string categories to enum cases and filter out invalid ones + let validCategories = uniqueCategoryStrings.compactMap { SoundCategory(from: $0) } + + // Always include "All" and filter other categories based on available sounds + let allCategories = [SoundCategory.all] + validCategories + + // Return sorted categories using the enum's sort order + return SoundCategory.sortedNoiseCategories.filter { allCategories.contains($0) } } - private var categoryDisplayName: (String) -> String { - return { category in - switch category { - case "all": return "All" - case "ambient": return "Ambient" - case "nature": return "Nature" - case "mechanical": return "Mechanical" - default: return category.capitalized - } - } - } // MARK: - Body var body: some View { @@ -103,9 +103,9 @@ struct SoundCategoryView: View { private var categoryTabs: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: UIConstants.Spacing.small) { - ForEach(categories, id: \.self) { category in + ForEach(categories) { category in CategoryTab( - title: categoryDisplayName(category), + title: category.displayName, isSelected: selectedCategory == category, count: getCategoryCount(for: category) ) { @@ -252,16 +252,7 @@ struct SoundCard: View { } private var soundIcon: String { - switch sound.category { - case "ambient": - return "waveform" - case "nature": - return "cloud.rain" - case "mechanical": - return "fan" - default: - return "speaker.wave.2" - } + return SoundCategory(from: sound.category)?.icon ?? "speaker.wave.2" } }