Import Coucou 0.1.9 (Louis-CFM/coucou@83708fe), Mac app only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import AVFoundation
|
||||
import AppKit
|
||||
|
||||
/// Preloaded WAV players with near-zero latency.
|
||||
/// Volume default 0.12 (matches prototype: gain ×6 then vol=0.12).
|
||||
@MainActor
|
||||
final class SoundEngine {
|
||||
static let shared = SoundEngine()
|
||||
|
||||
var enabled: Bool = true
|
||||
var volume: Float = 0.12 {
|
||||
didSet { players.values.forEach { $0.forEach { $0.volume = volume } } }
|
||||
}
|
||||
|
||||
// Pool of 3 players per sound to allow overlapping playback
|
||||
private var players: [String: [AVAudioPlayer]] = [:]
|
||||
|
||||
private init() {
|
||||
preload()
|
||||
}
|
||||
|
||||
private func preload() {
|
||||
let names = ["peek","open","close","hover","blip","slap","annoyed","dizzy","greet",
|
||||
"work","finish","error","approval","question","approve","gulp","tick",
|
||||
"send","love","pop","proud","wink","yawn","attach","think","search",
|
||||
"rate","sleep","greeting"]
|
||||
for name in names {
|
||||
guard let url = Bundle.main.url(forResource: name, withExtension: "wav", subdirectory: "sounds") else { continue }
|
||||
var pool: [AVAudioPlayer] = []
|
||||
for _ in 0..<3 {
|
||||
if let p = try? AVAudioPlayer(contentsOf: url) {
|
||||
p.volume = volume
|
||||
p.prepareToPlay()
|
||||
pool.append(p)
|
||||
}
|
||||
}
|
||||
if !pool.isEmpty { players[name] = pool }
|
||||
}
|
||||
}
|
||||
|
||||
/// Fade out all currently-playing instances of `name` over `duration` seconds,
|
||||
/// then stop and reset them so they can be reused.
|
||||
func fadeOut(_ name: String, duration: TimeInterval) {
|
||||
guard let pool = players[name] else { return }
|
||||
for player in pool where player.isPlaying {
|
||||
player.setVolume(0, fadeDuration: duration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak player] in
|
||||
guard let p = player else { return }
|
||||
p.stop()
|
||||
p.currentTime = 0
|
||||
p.volume = self.volume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func play(_ name: String) {
|
||||
guard enabled && AppState.shared.soundEnabled else { return }
|
||||
guard let pool = players[name] else { return }
|
||||
// Find a player that is not currently playing
|
||||
let player = pool.first { !$0.isPlaying } ?? pool[0]
|
||||
player.currentTime = 0
|
||||
player.volume = volume
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user