From 969ef23db258a91df1676d0451d1bd42a7c28f61 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 8 Sep 2025 08:07:32 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- TheNoiseClock/Models/Sound.swift | 3 +- TheNoiseClock/Models/SoundConfiguration.swift | 117 ++++++++++++++++++ TheNoiseClock/Resources/sounds.json | 50 ++++++++ TheNoiseClock/Services/AlarmService.swift | 3 +- TheNoiseClock/Services/NoisePlayer.swift | 50 +++++--- TheNoiseClock/ViewModels/NoiseViewModel.swift | 6 +- .../Noise/Components/SoundPickerView.swift | 11 +- 7 files changed, 216 insertions(+), 24 deletions(-) create mode 100644 TheNoiseClock/Models/SoundConfiguration.swift create mode 100644 TheNoiseClock/Resources/sounds.json diff --git a/TheNoiseClock/Models/Sound.swift b/TheNoiseClock/Models/Sound.swift index 9d2f6a0..bfeb4e1 100644 --- a/TheNoiseClock/Models/Sound.swift +++ b/TheNoiseClock/Models/Sound.swift @@ -9,12 +9,13 @@ import Foundation /// Sound data model for audio files struct Sound: Identifiable, Hashable { - let id = UUID() + let id: String let name: String let fileName: String // MARK: - Initialization init(name: String, fileName: String) { + self.id = fileName // Use fileName as stable identifier self.name = name self.fileName = fileName } diff --git a/TheNoiseClock/Models/SoundConfiguration.swift b/TheNoiseClock/Models/SoundConfiguration.swift new file mode 100644 index 0000000..a89646a --- /dev/null +++ b/TheNoiseClock/Models/SoundConfiguration.swift @@ -0,0 +1,117 @@ +// +// SoundConfiguration.swift +// TheNoiseClock +// +// Created by Matt Bruce on 9/7/25. +// + +import Foundation + +/// Configuration model for sound system loaded from JSON +struct SoundConfiguration: Codable { + let sounds: [SoundConfig] + let categories: [SoundCategory] + let settings: AudioSettings +} + +/// Individual sound configuration +struct SoundConfig: Codable, Identifiable { + let id: String + let name: String + let fileName: String + let category: String + let description: String + + /// Convert to Sound model for compatibility + func toSound() -> Sound { + return Sound(name: name, fileName: fileName) + } +} + +/// Sound category configuration +struct SoundCategory: Codable, Identifiable { + let id: String + let name: String + let description: String +} + +/// Audio settings configuration +struct AudioSettings: Codable { + let defaultVolume: Float + let defaultLoopCount: Int + let preloadSounds: Bool + let audioSessionCategory: String + let audioSessionMode: String + let audioSessionOptions: [String] +} + +/// Service for loading and managing sound configuration +class SoundConfigurationService { + static let shared = SoundConfigurationService() + + private var configuration: SoundConfiguration? + + private init() {} + + /// Load sound configuration from JSON file + func loadConfiguration() -> SoundConfiguration? { + guard let url = Bundle.main.url(forResource: "sounds", withExtension: "json") else { + print("❌ sounds.json not found in bundle") + return nil + } + + do { + let data = try Data(contentsOf: url) + let config = try JSONDecoder().decode(SoundConfiguration.self, from: data) + self.configuration = config + print("✅ Loaded sound configuration with \(config.sounds.count) sounds") + return config + } catch { + print("❌ Error loading sound configuration: \(error)") + return nil + } + } + + /// Get current configuration + func getConfiguration() -> SoundConfiguration? { + if configuration == nil { + return loadConfiguration() + } + return configuration + } + + /// Get all available sounds + func getAvailableSounds() -> [Sound] { + guard let config = getConfiguration() else { + print("⚠️ No configuration available, falling back to constants") + return getFallbackSounds() + } + + return config.sounds.map { $0.toSound() } + } + + /// Get sounds by category + func getSoundsByCategory(_ categoryId: String) -> [Sound] { + guard let config = getConfiguration() else { + return [] + } + + return config.sounds + .filter { $0.category == categoryId } + .map { $0.toSound() } + } + + /// Get audio settings + func getAudioSettings() -> AudioSettings? { + return getConfiguration()?.settings + } + + /// Fallback sounds if JSON loading fails + private func getFallbackSounds() -> [Sound] { + return [ + Sound(name: "White Noise", fileName: "white-noise.mp3"), + Sound(name: "Heavy Rain White Noise", fileName: "heavy-rain-white-noise.mp3"), + Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater-303207.mp3") + ] + } +} diff --git a/TheNoiseClock/Resources/sounds.json b/TheNoiseClock/Resources/sounds.json new file mode 100644 index 0000000..0d5d92a --- /dev/null +++ b/TheNoiseClock/Resources/sounds.json @@ -0,0 +1,50 @@ +{ + "sounds": [ + { + "id": "white-noise", + "name": "White Noise", + "fileName": "white-noise.mp3", + "category": "ambient", + "description": "Classic white noise for focus and relaxation" + }, + { + "id": "heavy-rain", + "name": "Heavy Rain White Noise", + "fileName": "heavy-rain-white-noise.mp3", + "category": "nature", + "description": "Heavy rainfall sounds for peaceful sleep" + }, + { + "id": "fan-noise", + "name": "Fan White Noise", + "fileName": "fan-white-noise-heater-303207.mp3", + "category": "mechanical", + "description": "Fan and heater sounds for consistent background noise" + } + ], + "categories": [ + { + "id": "ambient", + "name": "Ambient", + "description": "General ambient sounds" + }, + { + "id": "nature", + "name": "Nature", + "description": "Natural environmental sounds" + }, + { + "id": "mechanical", + "name": "Mechanical", + "description": "Mechanical and electronic sounds" + } + ], + "settings": { + "defaultVolume": 0.8, + "defaultLoopCount": -1, + "preloadSounds": true, + "audioSessionCategory": "playback", + "audioSessionMode": "default", + "audioSessionOptions": ["mixWithOthers"] + } +} diff --git a/TheNoiseClock/Services/AlarmService.swift b/TheNoiseClock/Services/AlarmService.swift index 81962ee..6d82903 100644 --- a/TheNoiseClock/Services/AlarmService.swift +++ b/TheNoiseClock/Services/AlarmService.swift @@ -81,7 +81,7 @@ class AlarmService { soundName: alarm.soundName ) let trigger = NotificationUtils.createCalendarTrigger(for: alarm.time) - await NotificationUtils.scheduleNotification( + _ = await NotificationUtils.scheduleNotification( identifier: alarm.id.uuidString, content: content, trigger: trigger @@ -120,3 +120,4 @@ class AlarmService { } } } + diff --git a/TheNoiseClock/Services/NoisePlayer.swift b/TheNoiseClock/Services/NoisePlayer.swift index 82b2c48..b820034 100644 --- a/TheNoiseClock/Services/NoisePlayer.swift +++ b/TheNoiseClock/Services/NoisePlayer.swift @@ -32,15 +32,22 @@ class NoisePlayer { } func playSound(_ sound: Sound) { + print("🎵 Attempting to play: \(sound.name)") + // Stop current sound if playing stopSound() + // Get or create player for this sound guard let player = players[sound.fileName] else { - print("Sound not preloaded: \(sound.fileName)") + print("❌ Sound not preloaded: \(sound.fileName)") + print("📁 Available sounds: \(players.keys)") return } currentPlayer = player - player.play() + let success = player.play() + print("🎵 Play result: \(success ? "SUCCESS" : "FAILED")") + print("🔊 Player isPlaying: \(player.isPlaying)") + print("🔊 Player volume: \(player.volume)") } func stopSound() { @@ -51,11 +58,17 @@ class NoisePlayer { // MARK: - Private Methods private func setupAudioSession() { do { - try AVAudioSession.sharedInstance().setCategory( - AudioConstants.AudioSession.category, - mode: AudioConstants.AudioSession.mode, - options: AudioConstants.AudioSession.options - ) + let settings = SoundConfigurationService.shared.getAudioSettings() + + // Use configuration settings or fall back to constants + let category = settings?.audioSessionCategory == "playback" ? + AVAudioSession.Category.playback : AudioConstants.AudioSession.category + let mode = settings?.audioSessionMode == "default" ? + AVAudioSession.Mode.default : AudioConstants.AudioSession.mode + let options: AVAudioSession.CategoryOptions = settings?.audioSessionOptions.contains("mixWithOthers") == true ? + [.mixWithOthers] : AudioConstants.AudioSession.options + + try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options) try AVAudioSession.sharedInstance().setActive(true) } catch { print("Error setting up audio session: \(error)") @@ -63,23 +76,32 @@ class NoisePlayer { } private func preloadSounds() { - for fileName in AudioConstants.SoundFiles.allFiles { - guard let url = Bundle.main.url(forResource: fileName, withExtension: nil) else { - print("Sound file not found: \(fileName)") + print("📁 Preloading audio files...") + + // Get sound configuration + let sounds = SoundConfigurationService.shared.getAvailableSounds() + let settings = SoundConfigurationService.shared.getAudioSettings() + + for sound in sounds { + guard let url = Bundle.main.url(forResource: sound.fileName, withExtension: nil) else { + print("❌ Sound file not found: \(sound.fileName)") continue } do { let player = try AVAudioPlayer(contentsOf: url) - player.numberOfLoops = AudioConstants.Playback.numberOfLoops - if AudioConstants.Playback.prepareToPlay { + player.numberOfLoops = settings?.defaultLoopCount ?? AudioConstants.Playback.numberOfLoops + player.volume = settings?.defaultVolume ?? AudioConstants.Volume.default + if settings?.preloadSounds ?? AudioConstants.Playback.prepareToPlay { player.prepareToPlay() } - players[fileName] = player + players[sound.fileName] = player + print("✅ Loaded: \(sound.name) (\(sound.fileName))") } catch { - print("Error preloading sound \(fileName): \(error)") + print("❌ Error preloading sound \(sound.fileName): \(error)") } } + print("📁 Preloading complete. Loaded \(players.count) sounds.") } private func stopAllSounds() { diff --git a/TheNoiseClock/ViewModels/NoiseViewModel.swift b/TheNoiseClock/ViewModels/NoiseViewModel.swift index 0c244fc..b14ee22 100644 --- a/TheNoiseClock/ViewModels/NoiseViewModel.swift +++ b/TheNoiseClock/ViewModels/NoiseViewModel.swift @@ -20,11 +20,7 @@ class NoiseViewModel { } var availableSounds: [Sound] { - [ - Sound(name: AudioConstants.SoundNames.whiteNoise, fileName: AudioConstants.SoundFiles.whiteNoise), - Sound(name: AudioConstants.SoundNames.heavyRain, fileName: AudioConstants.SoundFiles.heavyRain), - Sound(name: AudioConstants.SoundNames.fanNoise, fileName: AudioConstants.SoundFiles.fanNoise) - ] + return SoundConfigurationService.shared.getAvailableSounds() } // MARK: - Initialization diff --git a/TheNoiseClock/Views/Noise/Components/SoundPickerView.swift b/TheNoiseClock/Views/Noise/Components/SoundPickerView.swift index 8a809ba..d1bc3a5 100644 --- a/TheNoiseClock/Views/Noise/Components/SoundPickerView.swift +++ b/TheNoiseClock/Views/Noise/Components/SoundPickerView.swift @@ -16,10 +16,15 @@ struct SoundPickerView: View { // MARK: - Body var body: some View { - Picker("Select Noise", selection: $selectedSound) { - Text("Choose a sound").tag(nil as Sound?) + Picker("Select Noise", selection: Binding( + get: { selectedSound?.fileName }, + set: { fileName in + selectedSound = sounds.first { $0.fileName == fileName } + } + )) { + Text("Choose a sound").tag(nil as String?) ForEach(sounds) { sound in - Text(sound.name).tag(sound as Sound?) + Text(sound.name).tag(sound.fileName as String?) } } .pickerStyle(.menu)