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

This commit is contained in:
Matt Bruce 2025-09-08 08:07:32 -05:00
parent 204aabf8d2
commit 969ef23db2
7 changed files with 216 additions and 24 deletions

View File

@ -9,12 +9,13 @@ import Foundation
/// Sound data model for audio files /// Sound data model for audio files
struct Sound: Identifiable, Hashable { struct Sound: Identifiable, Hashable {
let id = UUID() let id: String
let name: String let name: String
let fileName: String let fileName: String
// MARK: - Initialization // MARK: - Initialization
init(name: String, fileName: String) { init(name: String, fileName: String) {
self.id = fileName // Use fileName as stable identifier
self.name = name self.name = name
self.fileName = fileName self.fileName = fileName
} }

View File

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

View File

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

View File

@ -81,7 +81,7 @@ class AlarmService {
soundName: alarm.soundName soundName: alarm.soundName
) )
let trigger = NotificationUtils.createCalendarTrigger(for: alarm.time) let trigger = NotificationUtils.createCalendarTrigger(for: alarm.time)
await NotificationUtils.scheduleNotification( _ = await NotificationUtils.scheduleNotification(
identifier: alarm.id.uuidString, identifier: alarm.id.uuidString,
content: content, content: content,
trigger: trigger trigger: trigger
@ -120,3 +120,4 @@ class AlarmService {
} }
} }
} }

View File

@ -32,15 +32,22 @@ class NoisePlayer {
} }
func playSound(_ sound: Sound) { func playSound(_ sound: Sound) {
print("🎵 Attempting to play: \(sound.name)")
// Stop current sound if playing
stopSound() stopSound()
// Get or create player for this sound
guard let player = players[sound.fileName] else { 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 return
} }
currentPlayer = player 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() { func stopSound() {
@ -51,11 +58,17 @@ class NoisePlayer {
// MARK: - Private Methods // MARK: - Private Methods
private func setupAudioSession() { private func setupAudioSession() {
do { do {
try AVAudioSession.sharedInstance().setCategory( let settings = SoundConfigurationService.shared.getAudioSettings()
AudioConstants.AudioSession.category,
mode: AudioConstants.AudioSession.mode, // Use configuration settings or fall back to constants
options: AudioConstants.AudioSession.options 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) try AVAudioSession.sharedInstance().setActive(true)
} catch { } catch {
print("Error setting up audio session: \(error)") print("Error setting up audio session: \(error)")
@ -63,23 +76,32 @@ class NoisePlayer {
} }
private func preloadSounds() { private func preloadSounds() {
for fileName in AudioConstants.SoundFiles.allFiles { print("📁 Preloading audio files...")
guard let url = Bundle.main.url(forResource: fileName, withExtension: nil) else {
print("Sound file not found: \(fileName)") // 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 continue
} }
do { do {
let player = try AVAudioPlayer(contentsOf: url) let player = try AVAudioPlayer(contentsOf: url)
player.numberOfLoops = AudioConstants.Playback.numberOfLoops player.numberOfLoops = settings?.defaultLoopCount ?? AudioConstants.Playback.numberOfLoops
if AudioConstants.Playback.prepareToPlay { player.volume = settings?.defaultVolume ?? AudioConstants.Volume.default
if settings?.preloadSounds ?? AudioConstants.Playback.prepareToPlay {
player.prepareToPlay() player.prepareToPlay()
} }
players[fileName] = player players[sound.fileName] = player
print("✅ Loaded: \(sound.name) (\(sound.fileName))")
} catch { } catch {
print("Error preloading sound \(fileName): \(error)") print("Error preloading sound \(sound.fileName): \(error)")
} }
} }
print("📁 Preloading complete. Loaded \(players.count) sounds.")
} }
private func stopAllSounds() { private func stopAllSounds() {

View File

@ -20,11 +20,7 @@ class NoiseViewModel {
} }
var availableSounds: [Sound] { var availableSounds: [Sound] {
[ return SoundConfigurationService.shared.getAvailableSounds()
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)
]
} }
// MARK: - Initialization // MARK: - Initialization

View File

@ -16,10 +16,15 @@ struct SoundPickerView: View {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
Picker("Select Noise", selection: $selectedSound) { Picker("Select Noise", selection: Binding(
Text("Choose a sound").tag(nil as Sound?) get: { selectedSound?.fileName },
set: { fileName in
selectedSound = sounds.first { $0.fileName == fileName }
}
)) {
Text("Choose a sound").tag(nil as String?)
ForEach(sounds) { sound in ForEach(sounds) { sound in
Text(sound.name).tag(sound as Sound?) Text(sound.name).tag(sound.fileName as String?)
} }
} }
.pickerStyle(.menu) .pickerStyle(.menu)