// // SoundManager.swift // Baccarat // // Manages game sound effects. // import AVFoundation import AudioToolbox import SwiftUI /// Types of sound effects used in the game. enum GameSound: String, CaseIterable { case chipPlace = "chip_place" // When placing a bet case chipStack = "chip_stack" // Stacking multiple chips case cardDeal = "card_deal" // Dealing a card case cardFlip = "card_flip" // Flipping a card face up case cardShuffle = "card_shuffle" // Shuffling the deck case win = "win" // Player wins case lose = "lose" // Player loses case push = "push" // Tie/push result case bigWin = "big_win" // Large payout case buttonTap = "button_tap" // UI button tap case newRound = "new_round" // Starting a new round case clearBets = "clear_bets" // Clearing all bets case gameOver = "game_over" // Out of chips / game over /// File extension for the sound file. var fileExtension: String { "mp3" } /// System sound ID to use as fallback when custom sound file is missing. /// These are built-in iOS sounds that approximate the intended effect. var fallbackSystemSound: SystemSoundID { switch self { case .chipPlace: return 1104 // Key press click case .chipStack: return 1105 // Keyboard key case .cardDeal: return 1306 // Swoosh case .cardFlip: return 1104 // Click case .cardShuffle: return 1110 // Swish case .win: return 1025 // Success chime case .lose: return 1053 // Error tone case .push: return 1057 // Neutral beep case .bigWin: return 1026 // Fanfare case .buttonTap: return 1104 // Tap case .newRound: return 1113 // Begin case .clearBets: return 1155 // Sweep case .gameOver: return 1073 // Sad trombone / failure } } /// Display name for settings. var displayName: String { switch self { case .chipPlace: return "Chip Place" case .chipStack: return "Chip Stack" case .cardDeal: return "Card Deal" case .cardFlip: return "Card Flip" case .cardShuffle: return "Card Shuffle" case .win: return "Win" case .lose: return "Lose" case .push: return "Push/Tie" case .bigWin: return "Big Win" case .buttonTap: return "Button Tap" case .newRound: return "New Round" case .clearBets: return "Clear Bets" case .gameOver: return "Game Over" } } } /// Manages playing game sound effects. @MainActor @Observable final class SoundManager { // MARK: - Singleton static let shared = SoundManager() // MARK: - Properties /// Whether sound effects are enabled. var soundEnabled: Bool = true { didSet { UserDefaults.standard.set(soundEnabled, forKey: "soundEnabled") } } /// Master volume (0.0 to 1.0). var volume: Float = 1.0 { didSet { UserDefaults.standard.set(volume, forKey: "soundVolume") updatePlayerVolumes() } } /// Whether to use system sounds as fallback (true until custom sounds are added). private var useSystemSoundsFallback: Bool = true /// Cache of audio players for quick playback. private var audioPlayers: [GameSound: AVAudioPlayer] = [:] // MARK: - Initialization private init() { // Load saved preferences soundEnabled = UserDefaults.standard.object(forKey: "soundEnabled") as? Bool ?? true volume = UserDefaults.standard.object(forKey: "soundVolume") as? Float ?? 1.0 // Configure audio session configureAudioSession() // Preload sounds preloadSounds() } // MARK: - Audio Session private func configureAudioSession() { do { try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) try AVAudioSession.sharedInstance().setActive(true) } catch { print("Failed to configure audio session: \(error)") } } // MARK: - Preloading /// Preloads all sound files for faster playback. private func preloadSounds() { var foundCustomSound = false for sound in GameSound.allCases { if let player = createPlayer(for: sound) { audioPlayers[sound] = player foundCustomSound = true } } // If any custom sounds are found, disable fallback for all sounds useSystemSoundsFallback = !foundCustomSound } private func createPlayer(for sound: GameSound) -> AVAudioPlayer? { guard let url = Bundle.main.url( forResource: sound.rawValue, withExtension: sound.fileExtension ) else { // Sound file not found - will use system sound fallback return nil } do { let player = try AVAudioPlayer(contentsOf: url) player.prepareToPlay() player.volume = volume return player } catch { print("Failed to create audio player for \(sound.rawValue): \(error)") return nil } } // MARK: - Playback /// Plays a sound effect. /// - Parameter sound: The sound to play. func play(_ sound: GameSound) { guard soundEnabled else { return } // Try custom sound first if let player = audioPlayers[sound] { player.currentTime = 0 player.play() } else { // Fall back to system sound playSystemSound(sound.fallbackSystemSound) } } /// Plays a system sound by ID. private func playSystemSound(_ soundID: SystemSoundID) { // Only play if volume is above threshold guard volume > 0.1 else { return } AudioServicesPlaySystemSound(soundID) } /// Plays a sound effect with a delay. /// - Parameters: /// - sound: The sound to play. /// - delay: Delay in seconds before playing. func play(_ sound: GameSound, delay: TimeInterval) { Task { try? await Task.sleep(for: .seconds(delay)) play(sound) } } /// Plays multiple sounds in sequence. /// - Parameters: /// - sounds: Array of sounds to play. /// - interval: Time between each sound. func playSequence(_ sounds: [GameSound], interval: TimeInterval = 0.1) { for (index, sound) in sounds.enumerated() { play(sound, delay: TimeInterval(index) * interval) } } // MARK: - Volume private func updatePlayerVolumes() { for player in audioPlayers.values { player.volume = volume } } // MARK: - Haptics /// Plays a light haptic feedback. func hapticLight() { let impact = UIImpactFeedbackGenerator(style: .light) impact.impactOccurred() } /// Plays a medium haptic feedback. func hapticMedium() { let impact = UIImpactFeedbackGenerator(style: .medium) impact.impactOccurred() } /// Plays a success haptic notification. func hapticSuccess() { let notification = UINotificationFeedbackGenerator() notification.notificationOccurred(.success) } /// Plays an error haptic notification. func hapticError() { let notification = UINotificationFeedbackGenerator() notification.notificationOccurred(.error) } } // MARK: - Convenience Extensions extension SoundManager { /// Plays chip placement sound with haptic. func playChipPlace() { play(.chipPlace) hapticLight() } /// Plays card deal sound. func playCardDeal() { play(.cardDeal) } /// Plays card flip sound. func playCardFlip() { play(.cardFlip) hapticLight() } /// Plays win sound with haptic. func playWin(isBigWin: Bool = false) { play(isBigWin ? .bigWin : .win) hapticSuccess() } /// Plays lose sound with haptic. func playLose() { play(.lose) hapticError() } /// Plays push/tie sound. func playPush() { play(.push) hapticMedium() } /// Plays game over sound with haptic. func playGameOver() { play(.gameOver) hapticError() } }