CasinoGames/Baccarat/Audio/SoundManager.swift

290 lines
8.5 KiB
Swift

//
// 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()
}
}