Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
204aabf8d2
commit
969ef23db2
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
117
TheNoiseClock/Models/SoundConfiguration.swift
Normal file
117
TheNoiseClock/Models/SoundConfiguration.swift
Normal 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")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
50
TheNoiseClock/Resources/sounds.json
Normal file
50
TheNoiseClock/Resources/sounds.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user