diff --git a/AUDIO_MANAGEMENT_GUIDE.md b/AUDIO_MANAGEMENT_GUIDE.md new file mode 100644 index 0000000..fac3905 --- /dev/null +++ b/AUDIO_MANAGEMENT_GUIDE.md @@ -0,0 +1,171 @@ +# 🎵 Audio File Management Guide + +## 📊 **Scale Considerations** + +### **Small Scale (1-10 files)** +- ✅ **Current approach works fine** +- ✅ Keep files in root bundle +- ✅ Simple JSON configuration + +### **Medium Scale (10-50 files)** +- ⚠️ **Consider bundles for organization** +- ⚠️ Group by category (Ambient, Nature, Mechanical) +- ⚠️ Use preload strategy: "category" + +### **Large Scale (50+ files)** +- 🚨 **Definitely use bundles** +- 🚨 Implement lazy loading +- 🚨 Consider dynamic downloads + +## 🎯 **Recommended Approaches** + +### **Option 1: Category-Based Bundles (Recommended)** + +``` +TheNoiseClock/ +├── Resources/ +│ ├── sounds.json +│ ├── Ambient.bundle/ +│ │ ├── white-noise.mp3 +│ │ ├── pink-noise.mp3 +│ │ └── brown-noise.mp3 +│ ├── Nature.bundle/ +│ │ ├── rain.mp3 +│ │ ├── ocean.mp3 +│ │ └── forest.mp3 +│ └── Mechanical.bundle/ +│ ├── fan.mp3 +│ ├── air-conditioner.mp3 +│ └── washing-machine.mp3 +``` + +**Benefits:** +- ✅ **Organized by category** - Easy to manage +- ✅ **Faster loading** - Only load sounds from active category +- ✅ **Smaller memory footprint** - Don't preload everything +- ✅ **Easy to add new categories** - Just create new bundle +- ✅ **Clean project structure** - No file clutter + +### **Option 2: Single Bundle with Subfolders** + +``` +TheNoiseClock/ +├── Resources/ +│ ├── sounds.json +│ └── Sounds.bundle/ +│ ├── Ambient/ +│ ├── Nature/ +│ └── Mechanical/ +``` + +**Benefits:** +- ✅ **Single bundle** - Easier to manage as one unit +- ✅ **Organized structure** - Still categorized +- ✅ **All sounds in one place** - Simpler distribution + +## 🚀 **Implementation Steps** + +### **Step 1: Create Bundles in Xcode** + +1. **Right-click** on `TheNoiseClock` folder +2. **Select "New Group"** and name it `Ambient.bundle` +3. **Add audio files** to the bundle +4. **Repeat** for other categories + +### **Step 2: Update JSON Configuration** + +```json +{ + "sounds": [ + { + "id": "white-noise", + "name": "White Noise", + "fileName": "white-noise.mp3", + "category": "ambient", + "bundleName": "Ambient" + } + ], + "settings": { + "preloadStrategy": "category" + } +} +``` + +### **Step 3: Update Code (Already Done)** + +The code has been updated to support: +- ✅ Bundle-based file loading +- ✅ Category-based preloading +- ✅ Fallback to direct files + +## 📈 **Performance Optimizations** + +### **Preload Strategies** + +1. **"all"** - Load all sounds at startup + - ✅ Fast switching between sounds + - ❌ High memory usage + - ❌ Slow startup + +2. **"category"** - Load sounds by category + - ✅ Balanced memory usage + - ✅ Fast category switching + - ⚠️ Slight delay when switching categories + +3. **"none"** - Load sounds on demand + - ✅ Minimal memory usage + - ✅ Fast startup + - ❌ Delay when playing new sounds + +### **Memory Management** + +```swift +// Example: Load only active category +func loadCategory(_ categoryId: String) { + let sounds = SoundConfigurationService.shared.getSoundsByCategory(categoryId) + // Preload only these sounds +} +``` + +## 🔄 **Migration Path** + +### **From Current Setup to Bundles** + +1. **Keep current setup working** ✅ +2. **Create bundles** for new sounds +3. **Update JSON** to use bundle names +4. **Test thoroughly** before removing old files +5. **Gradually migrate** existing sounds + +### **Backward Compatibility** + +The code supports both approaches: +- ✅ **Direct files** (current setup) +- ✅ **Bundle files** (new approach) +- ✅ **Mixed approach** (during migration) + +## 📱 **App Store Considerations** + +### **Bundle Size Limits** + +- **iOS App Store**: 4GB limit +- **TestFlight**: 4GB limit +- **Enterprise**: No limit + +### **Best Practices** + +1. **Compress audio files** - Use AAC format +2. **Optimize bitrates** - 128kbps is usually sufficient +3. **Consider dynamic downloads** - For very large collections +4. **Use bundles** - Better organization and loading + +## 🎯 **Recommendation for Your Use Case** + +Based on your current setup, I recommend: + +1. **Keep current setup** for now (it works!) +2. **Use bundles** for new sound categories +3. **Implement category-based preloading** +4. **Consider dynamic downloads** if you exceed 100+ files + +The code is already prepared for this migration path! 🚀 diff --git a/TheNoiseClock/Models/Sound.swift b/TheNoiseClock/Models/Sound.swift index bfeb4e1..b2ecb27 100644 --- a/TheNoiseClock/Models/Sound.swift +++ b/TheNoiseClock/Models/Sound.swift @@ -12,12 +12,14 @@ struct Sound: Identifiable, Hashable { let id: String let name: String let fileName: String + let bundleName: String? // Optional bundle name for organization // MARK: - Initialization - init(name: String, fileName: String) { + init(name: String, fileName: String, bundleName: String? = nil) { self.id = fileName // Use fileName as stable identifier self.name = name self.fileName = fileName + self.bundleName = bundleName } // MARK: - Hashable diff --git a/TheNoiseClock/Models/SoundConfiguration.swift b/TheNoiseClock/Models/SoundConfiguration.swift index a89646a..cedbf76 100644 --- a/TheNoiseClock/Models/SoundConfiguration.swift +++ b/TheNoiseClock/Models/SoundConfiguration.swift @@ -21,10 +21,11 @@ struct SoundConfig: Codable, Identifiable { let fileName: String let category: String let description: String + let bundleName: String? // Optional bundle name for organization /// Convert to Sound model for compatibility func toSound() -> Sound { - return Sound(name: name, fileName: fileName) + return Sound(name: name, fileName: fileName, bundleName: bundleName) } } @@ -33,6 +34,7 @@ struct SoundCategory: Codable, Identifiable { let id: String let name: String let description: String + let bundleName: String? // Optional bundle name for this category } /// Audio settings configuration @@ -40,6 +42,7 @@ struct AudioSettings: Codable { let defaultVolume: Float let defaultLoopCount: Int let preloadSounds: Bool + let preloadStrategy: String // "all", "category", "none" let audioSessionCategory: String let audioSessionMode: String let audioSessionOptions: [String] @@ -101,6 +104,22 @@ class SoundConfigurationService { .map { $0.toSound() } } + /// Get sounds by bundle name + func getSoundsByBundle(_ bundleName: String) -> [Sound] { + guard let config = getConfiguration() else { + return [] + } + + return config.sounds + .filter { $0.bundleName == bundleName } + .map { $0.toSound() } + } + + /// Get available categories + func getAvailableCategories() -> [SoundCategory] { + return getConfiguration()?.categories ?? [] + } + /// Get audio settings func getAudioSettings() -> AudioSettings? { return getConfiguration()?.settings @@ -109,9 +128,9 @@ class SoundConfigurationService { /// 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") + Sound(name: "White Noise", fileName: "white-noise.mp3", bundleName: "Ambient"), + Sound(name: "Heavy Rain White Noise", fileName: "heavy-rain-white-noise.mp3", bundleName: "Nature"), + Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater.mp3", bundleName: "Mechanical") ] } -} +} \ No newline at end of file diff --git a/TheNoiseClock/Resources/white-noise.mp3 b/TheNoiseClock/Resources/Ambient.bundle/white-noise.mp3 similarity index 100% rename from TheNoiseClock/Resources/white-noise.mp3 rename to TheNoiseClock/Resources/Ambient.bundle/white-noise.mp3 diff --git a/TheNoiseClock/Resources/fan-white-noise-heater-303207.mp3 b/TheNoiseClock/Resources/Mechanical.bundle/fan-white-noise-heater.mp3 similarity index 100% rename from TheNoiseClock/Resources/fan-white-noise-heater-303207.mp3 rename to TheNoiseClock/Resources/Mechanical.bundle/fan-white-noise-heater.mp3 diff --git a/TheNoiseClock/Resources/heavy-rain-white-noise.mp3 b/TheNoiseClock/Resources/Nature.bundle/heavy-rain-white-noise.mp3 similarity index 100% rename from TheNoiseClock/Resources/heavy-rain-white-noise.mp3 rename to TheNoiseClock/Resources/Nature.bundle/heavy-rain-white-noise.mp3 diff --git a/TheNoiseClock/Resources/sounds.json b/TheNoiseClock/Resources/sounds.json index 0d5d92a..e10dddd 100644 --- a/TheNoiseClock/Resources/sounds.json +++ b/TheNoiseClock/Resources/sounds.json @@ -5,44 +5,51 @@ "name": "White Noise", "fileName": "white-noise.mp3", "category": "ambient", - "description": "Classic white noise for focus and relaxation" + "description": "Classic white noise for focus and relaxation", + "bundleName": "Ambient" }, { "id": "heavy-rain", "name": "Heavy Rain White Noise", "fileName": "heavy-rain-white-noise.mp3", "category": "nature", - "description": "Heavy rainfall sounds for peaceful sleep" + "description": "Heavy rainfall sounds for peaceful sleep", + "bundleName": "Nature" }, { "id": "fan-noise", "name": "Fan White Noise", - "fileName": "fan-white-noise-heater-303207.mp3", + "fileName": "fan-white-noise-heater.mp3", "category": "mechanical", - "description": "Fan and heater sounds for consistent background noise" + "description": "Fan and heater sounds for consistent background noise", + "bundleName": "Mechanical" } ], "categories": [ { "id": "ambient", "name": "Ambient", - "description": "General ambient sounds" + "description": "General ambient sounds", + "bundleName": "Ambient" }, { "id": "nature", "name": "Nature", - "description": "Natural environmental sounds" + "description": "Natural environmental sounds", + "bundleName": "Nature" }, { "id": "mechanical", "name": "Mechanical", - "description": "Mechanical and electronic sounds" + "description": "Mechanical and electronic sounds", + "bundleName": "Mechanical" } ], "settings": { "defaultVolume": 0.8, "defaultLoopCount": -1, "preloadSounds": true, + "preloadStrategy": "category", "audioSessionCategory": "playback", "audioSessionMode": "default", "audioSessionOptions": ["mixWithOthers"] diff --git a/TheNoiseClock/Services/NoisePlayer.swift b/TheNoiseClock/Services/NoisePlayer.swift index b820034..6835935 100644 --- a/TheNoiseClock/Services/NoisePlayer.swift +++ b/TheNoiseClock/Services/NoisePlayer.swift @@ -40,7 +40,27 @@ class NoisePlayer { guard let player = players[sound.fileName] else { print("❌ Sound not preloaded: \(sound.fileName)") print("📁 Available sounds: \(players.keys)") - return + + // Try to load the sound dynamically as fallback + guard let fileUrl = getURL(for: sound) else { + print("❌ Sound file not found: \(sound.fileName)") + return + } + + do { + let newPlayer = try AVAudioPlayer(contentsOf: fileUrl) + newPlayer.numberOfLoops = AudioConstants.Playback.numberOfLoops + newPlayer.volume = AudioConstants.Volume.default + newPlayer.prepareToPlay() + players[sound.fileName] = newPlayer + currentPlayer = newPlayer + let success = newPlayer.play() + print("🎵 Fallback play result: \(success ? "SUCCESS" : "FAILED")") + return + } catch { + print("❌ Error creating fallback player: \(error)") + return + } } currentPlayer = player @@ -56,6 +76,30 @@ class NoisePlayer { } // MARK: - Private Methods + + /// Helper method to get URL for sound file, handling bundles and direct paths + private func getURL(for sound: Sound) -> URL? { + // If sound has a bundle name, look in that bundle first + if let bundleName = sound.bundleName { + if let bundleURL = Bundle.main.url(forResource: bundleName, withExtension: "bundle"), + let bundle = Bundle(url: bundleURL) { + return bundle.url(forResource: sound.fileName, withExtension: nil) + } + } + + // Fallback to direct file path + if sound.fileName.contains("/") { + // Path includes subfolder (e.g., "Sounds/white-noise.mp3") + let components = sound.fileName.components(separatedBy: "/") + let fileName = components.last! + let subfolder = components.dropLast().joined(separator: "/") + return Bundle.main.url(forResource: fileName, withExtension: nil, subdirectory: subfolder) + } else { + // Direct file path (fallback) + return Bundle.main.url(forResource: sound.fileName, withExtension: nil) + } + } + private func setupAudioSession() { do { let settings = SoundConfigurationService.shared.getAudioSettings() @@ -83,13 +127,13 @@ class NoisePlayer { let settings = SoundConfigurationService.shared.getAudioSettings() for sound in sounds { - guard let url = Bundle.main.url(forResource: sound.fileName, withExtension: nil) else { + guard let fileUrl = getURL(for: sound) else { print("❌ Sound file not found: \(sound.fileName)") continue } do { - let player = try AVAudioPlayer(contentsOf: url) + let player = try AVAudioPlayer(contentsOf: fileUrl) player.numberOfLoops = settings?.defaultLoopCount ?? AudioConstants.Playback.numberOfLoops player.volume = settings?.defaultVolume ?? AudioConstants.Volume.default if settings?.preloadSounds ?? AudioConstants.Playback.prepareToPlay {