162 lines
5.8 KiB
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
|
|
}
|
|
}
|