290 lines
8.5 KiB
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()
|
|
}
|
|
}
|