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

This commit is contained in:
Matt Bruce 2025-09-08 08:22:20 -05:00
parent 45a4a6067b
commit 27e949b752
8 changed files with 259 additions and 16 deletions

171
AUDIO_MANAGEMENT_GUIDE.md Normal file
View File

@ -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! 🚀

View File

@ -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

View File

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

View File

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

View File

@ -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 {