TheNoiseClock/TheNoiseClock/Services/NoisePlayer.swift

162 lines
5.8 KiB
Swift

//
// NoisePlayer.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import AVFoundation
import Observation
/// Audio playback service for white noise and ambient sounds
@Observable
class NoisePlayer {
// MARK: - Singleton
static let shared = NoisePlayer()
// MARK: - Properties
private var players: [String: AVAudioPlayer] = [:]
private var currentPlayer: AVAudioPlayer?
// MARK: - Initialization
private init() {
setupAudioSession()
preloadSounds()
}
deinit {
stopAllSounds()
}
// MARK: - Public Interface
var isPlaying: Bool {
return currentPlayer?.isPlaying ?? false
}
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("📁 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")")
print("🔊 Player isPlaying: \(player.isPlaying)")
print("🔊 Player volume: \(player.volume)")
}
func stopSound() {
currentPlayer?.stop()
currentPlayer = nil
}
// 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()
// 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)")
}
}
private func preloadSounds() {
print("📁 Preloading audio files...")
// Get sound configuration
let sounds = SoundConfigurationService.shared.getAvailableSounds()
let settings = SoundConfigurationService.shared.getAudioSettings()
for sound in sounds {
guard let fileUrl = getURL(for: sound) else {
print("❌ Sound file not found: \(sound.fileName)")
continue
}
do {
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 {
player.prepareToPlay()
}
players[sound.fileName] = player
print("✅ Loaded: \(sound.name) (\(sound.fileName))")
} catch {
print("❌ Error preloading sound \(sound.fileName): \(error)")
}
}
print("📁 Preloading complete. Loaded \(players.count) sounds.")
}
private func stopAllSounds() {
for player in players.values {
player.stop()
}
players.removeAll()
currentPlayer = nil
}
}