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

This commit is contained in:
Matt Bruce 2025-09-09 09:38:16 -05:00
parent 6d3837022d
commit 706aba5d30
26 changed files with 280 additions and 78 deletions

View File

@ -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

View File

@ -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 }
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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,

View File

@ -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": {

View File

@ -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

View File

@ -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"
}
}