Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
45a4a6067b
commit
27e949b752
171
AUDIO_MANAGEMENT_GUIDE.md
Normal file
171
AUDIO_MANAGEMENT_GUIDE.md
Normal 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! 🚀
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
|
||||
@ -40,9 +40,29 @@ class NoisePlayer {
|
||||
guard let player = players[sound.fileName] else {
|
||||
print("❌ Sound not preloaded: \(sound.fileName)")
|
||||
print("📁 Available sounds: \(players.keys)")
|
||||
|
||||
// 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
|
||||
let success = player.play()
|
||||
print("🎵 Play result: \(success ? "SUCCESS" : "FAILED")")
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user