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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user