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 id: String
|
||||||
let name: String
|
let name: String
|
||||||
let fileName: String
|
let fileName: String
|
||||||
|
let bundleName: String? // Optional bundle name for organization
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init(name: String, fileName: String) {
|
init(name: String, fileName: String, bundleName: String? = nil) {
|
||||||
self.id = fileName // Use fileName as stable identifier
|
self.id = fileName // Use fileName as stable identifier
|
||||||
self.name = name
|
self.name = name
|
||||||
self.fileName = fileName
|
self.fileName = fileName
|
||||||
|
self.bundleName = bundleName
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Hashable
|
// MARK: - Hashable
|
||||||
|
|||||||
@ -21,10 +21,11 @@ struct SoundConfig: Codable, Identifiable {
|
|||||||
let fileName: String
|
let fileName: String
|
||||||
let category: String
|
let category: String
|
||||||
let description: String
|
let description: String
|
||||||
|
let bundleName: String? // Optional bundle name for organization
|
||||||
|
|
||||||
/// Convert to Sound model for compatibility
|
/// Convert to Sound model for compatibility
|
||||||
func toSound() -> Sound {
|
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 id: String
|
||||||
let name: String
|
let name: String
|
||||||
let description: String
|
let description: String
|
||||||
|
let bundleName: String? // Optional bundle name for this category
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Audio settings configuration
|
/// Audio settings configuration
|
||||||
@ -40,6 +42,7 @@ struct AudioSettings: Codable {
|
|||||||
let defaultVolume: Float
|
let defaultVolume: Float
|
||||||
let defaultLoopCount: Int
|
let defaultLoopCount: Int
|
||||||
let preloadSounds: Bool
|
let preloadSounds: Bool
|
||||||
|
let preloadStrategy: String // "all", "category", "none"
|
||||||
let audioSessionCategory: String
|
let audioSessionCategory: String
|
||||||
let audioSessionMode: String
|
let audioSessionMode: String
|
||||||
let audioSessionOptions: [String]
|
let audioSessionOptions: [String]
|
||||||
@ -101,6 +104,22 @@ class SoundConfigurationService {
|
|||||||
.map { $0.toSound() }
|
.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
|
/// Get audio settings
|
||||||
func getAudioSettings() -> AudioSettings? {
|
func getAudioSettings() -> AudioSettings? {
|
||||||
return getConfiguration()?.settings
|
return getConfiguration()?.settings
|
||||||
@ -109,9 +128,9 @@ class SoundConfigurationService {
|
|||||||
/// Fallback sounds if JSON loading fails
|
/// Fallback sounds if JSON loading fails
|
||||||
private func getFallbackSounds() -> [Sound] {
|
private func getFallbackSounds() -> [Sound] {
|
||||||
return [
|
return [
|
||||||
Sound(name: "White Noise", fileName: "white-noise.mp3"),
|
Sound(name: "White Noise", fileName: "white-noise.mp3", bundleName: "Ambient"),
|
||||||
Sound(name: "Heavy Rain White Noise", fileName: "heavy-rain-white-noise.mp3"),
|
Sound(name: "Heavy Rain White Noise", fileName: "heavy-rain-white-noise.mp3", bundleName: "Nature"),
|
||||||
Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater-303207.mp3")
|
Sound(name: "Fan White Noise", fileName: "fan-white-noise-heater.mp3", bundleName: "Mechanical")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,44 +5,51 @@
|
|||||||
"name": "White Noise",
|
"name": "White Noise",
|
||||||
"fileName": "white-noise.mp3",
|
"fileName": "white-noise.mp3",
|
||||||
"category": "ambient",
|
"category": "ambient",
|
||||||
"description": "Classic white noise for focus and relaxation"
|
"description": "Classic white noise for focus and relaxation",
|
||||||
|
"bundleName": "Ambient"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "heavy-rain",
|
"id": "heavy-rain",
|
||||||
"name": "Heavy Rain White Noise",
|
"name": "Heavy Rain White Noise",
|
||||||
"fileName": "heavy-rain-white-noise.mp3",
|
"fileName": "heavy-rain-white-noise.mp3",
|
||||||
"category": "nature",
|
"category": "nature",
|
||||||
"description": "Heavy rainfall sounds for peaceful sleep"
|
"description": "Heavy rainfall sounds for peaceful sleep",
|
||||||
|
"bundleName": "Nature"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fan-noise",
|
"id": "fan-noise",
|
||||||
"name": "Fan White Noise",
|
"name": "Fan White Noise",
|
||||||
"fileName": "fan-white-noise-heater-303207.mp3",
|
"fileName": "fan-white-noise-heater.mp3",
|
||||||
"category": "mechanical",
|
"category": "mechanical",
|
||||||
"description": "Fan and heater sounds for consistent background noise"
|
"description": "Fan and heater sounds for consistent background noise",
|
||||||
|
"bundleName": "Mechanical"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{
|
||||||
"id": "ambient",
|
"id": "ambient",
|
||||||
"name": "Ambient",
|
"name": "Ambient",
|
||||||
"description": "General ambient sounds"
|
"description": "General ambient sounds",
|
||||||
|
"bundleName": "Ambient"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nature",
|
"id": "nature",
|
||||||
"name": "Nature",
|
"name": "Nature",
|
||||||
"description": "Natural environmental sounds"
|
"description": "Natural environmental sounds",
|
||||||
|
"bundleName": "Nature"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mechanical",
|
"id": "mechanical",
|
||||||
"name": "Mechanical",
|
"name": "Mechanical",
|
||||||
"description": "Mechanical and electronic sounds"
|
"description": "Mechanical and electronic sounds",
|
||||||
|
"bundleName": "Mechanical"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"defaultVolume": 0.8,
|
"defaultVolume": 0.8,
|
||||||
"defaultLoopCount": -1,
|
"defaultLoopCount": -1,
|
||||||
"preloadSounds": true,
|
"preloadSounds": true,
|
||||||
|
"preloadStrategy": "category",
|
||||||
"audioSessionCategory": "playback",
|
"audioSessionCategory": "playback",
|
||||||
"audioSessionMode": "default",
|
"audioSessionMode": "default",
|
||||||
"audioSessionOptions": ["mixWithOthers"]
|
"audioSessionOptions": ["mixWithOthers"]
|
||||||
|
|||||||
@ -40,7 +40,27 @@ class NoisePlayer {
|
|||||||
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)")
|
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
|
currentPlayer = player
|
||||||
@ -56,6 +76,30 @@ class NoisePlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// 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() {
|
private func setupAudioSession() {
|
||||||
do {
|
do {
|
||||||
let settings = SoundConfigurationService.shared.getAudioSettings()
|
let settings = SoundConfigurationService.shared.getAudioSettings()
|
||||||
@ -83,13 +127,13 @@ class NoisePlayer {
|
|||||||
let settings = SoundConfigurationService.shared.getAudioSettings()
|
let settings = SoundConfigurationService.shared.getAudioSettings()
|
||||||
|
|
||||||
for sound in sounds {
|
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)")
|
print("❌ Sound file not found: \(sound.fileName)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let player = try AVAudioPlayer(contentsOf: url)
|
let player = try AVAudioPlayer(contentsOf: fileUrl)
|
||||||
player.numberOfLoops = settings?.defaultLoopCount ?? AudioConstants.Playback.numberOfLoops
|
player.numberOfLoops = settings?.defaultLoopCount ?? AudioConstants.Playback.numberOfLoops
|
||||||
player.volume = settings?.defaultVolume ?? AudioConstants.Volume.default
|
player.volume = settings?.defaultVolume ?? AudioConstants.Volume.default
|
||||||
if settings?.preloadSounds ?? AudioConstants.Playback.prepareToPlay {
|
if settings?.preloadSounds ?? AudioConstants.Playback.prepareToPlay {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user