|
| 1 | +final class NSSound { |
| 2 | + static func beep() { played += 1 } |
| 3 | + static var created = 0 |
| 4 | + static var played = 0 |
| 5 | + static var createdHook: (() -> Void)? |
| 6 | + static var currentOutput: () -> String? = { nil } |
| 7 | + static var lastPinnedOutput: String? |
| 8 | + static var lastOutput: String? |
| 9 | + var playbackDeviceIdentifier: String? |
| 10 | + var volume: Float = 1 |
| 11 | + init?(contentsOfFile: String, byReference: Bool) { Self.created += 1; Self.createdHook?() } |
| 12 | + @discardableResult func play() -> Bool { Self.played += 1; Self.lastPinnedOutput = playbackDeviceIdentifier; Self.lastOutput = playbackDeviceIdentifier ?? Self.currentOutput(); return true } |
| 13 | +} |
| 14 | +final class Bundle { |
| 15 | + static let main = Bundle() |
| 16 | + func path(forResource: String, ofType: String) -> String? { "/test/sent.mp3" } |
| 17 | +} |
| 18 | +var device: AudioDeviceID? = 7 |
| 19 | +var systemDevice: AudioDeviceID? = 9 |
| 20 | +var systemMutes: [UInt32: UInt32] = [:] |
| 21 | +var systemVolumes: [UInt32: Float32] = [:] |
| 22 | +var uid: String? = "speakers" |
| 23 | +var mutes: [UInt32: UInt32] = [:] |
| 24 | +var volumes: [UInt32: Float32] = [:] |
| 25 | +var channels: [UInt32]? = [1, 2] |
| 26 | +func readTestProperty(_ object: AudioObjectID, _ address: UnsafePointer<AudioObjectPropertyAddress>, _ qualifierSize: UInt32, _ qualifier: UnsafeRawPointer?, _ size: UnsafeMutablePointer<UInt32>, _ data: UnsafeMutableRawPointer) -> OSStatus { |
| 27 | + switch address.pointee.mSelector { |
| 28 | + case kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDefaultSystemOutputDevice: |
| 29 | + let selected = address.pointee.mSelector == kAudioHardwarePropertyDefaultSystemOutputDevice ? systemDevice : device |
| 30 | + guard let selected else { return -1 } |
| 31 | + data.assumingMemoryBound(to: AudioDeviceID.self).pointee = selected |
| 32 | + case kAudioDevicePropertyDeviceUID: |
| 33 | + guard let uid else { return -1 } |
| 34 | + data.assumingMemoryBound(to: Optional<CFString>.self).pointee = uid as CFString |
| 35 | + case kAudioDevicePropertyMute: |
| 36 | + guard let value = (object == systemDevice ? systemMutes : mutes)[address.pointee.mElement] else { return -1 } |
| 37 | + data.assumingMemoryBound(to: UInt32.self).pointee = value |
| 38 | + case kAudioDevicePropertyVolumeScalar: |
| 39 | + guard let value = (object == systemDevice ? systemVolumes : volumes)[address.pointee.mElement] else { return -1 } |
| 40 | + data.assumingMemoryBound(to: Float32.self).pointee = value |
| 41 | + case kAudioDevicePropertyPreferredChannelsForStereo: |
| 42 | + guard let channels else { return -1 } |
| 43 | + for (i, value) in channels.enumerated() { data.assumingMemoryBound(to: UInt32.self)[i] = value } |
| 44 | + default: return -1 |
| 45 | + } |
| 46 | + return noErr |
| 47 | +} |
| 48 | +var failures = 0 |
| 49 | +var checks = 0 |
| 50 | +func reset() { |
| 51 | + device = 7; systemDevice = 9; systemMutes = [:]; systemVolumes = [:]; uid = "speakers"; mutes = [:]; volumes = [:]; channels = [1, 2] |
| 52 | + NSSound.created = 0; NSSound.played = 0; NSSound.lastOutput = nil; NSSound.lastPinnedOutput = nil; NSSound.createdHook = nil; NSSound.currentOutput = { uid } |
| 53 | +} |
| 54 | +func check(_ name: String, _ condition: Bool) { |
| 55 | + checks += 1 |
| 56 | + if !condition { failures += 1 } |
| 57 | + print("\(condition ? "PASS" : "FAIL") \(name)") |
| 58 | +} |
| 59 | +func suppressed(_ name: String) { |
| 60 | + playSoundEffect(.sent) |
| 61 | + check(name, NSSound.created == 0 && NSSound.played == 0) |
| 62 | +} |
| 63 | +reset(); mutes[0] = 1; suppressed("system mute skips sound allocation and playback") |
| 64 | +reset(); volumes[0] = 0; suppressed("zero master volume skips audio activation") |
| 65 | +reset(); mutes = [1: 1, 2: 1]; suppressed("both output channels muted") |
| 66 | +reset(); volumes = [1: 0, 2: 0]; suppressed("zero volume on both channels") |
| 67 | +reset(); mutes[1] = 1; volumes[2] = 0; suppressed("mixed mute and zero volume remains silent") |
| 68 | +reset(); device = nil; suppressed("missing default output does not fall back to another device") |
| 69 | +reset(); device = 0; suppressed("unknown device is not activated") |
| 70 | +reset(); uid = nil; playSoundEffect(.sent) |
| 71 | +check("an unreadable device UID does not block an audible default output",NSSound.played == 1) |
| 72 | +reset(); volumes = [1: 0, 2: 0.5]; playSoundEffect(.sent) |
| 73 | +check("one audible channel still plays", NSSound.created == 1 && NSSound.played == 1) |
| 74 | +reset(); channels = nil; playSoundEffect(.sent) |
| 75 | +check("fixed-volume output without channel controls remains supported", NSSound.played == 1) |
| 76 | +check("sound retains default-output routing without pinning a UID", NSSound.lastPinnedOutput == nil) |
| 77 | +uid = "wired-headphones"; playSoundEffect(.sent) |
| 78 | +check("manual device changes are read on the next sound", NSSound.lastOutput == "wired-headphones") |
| 79 | +mutes[0] = 1; let prior = NSSound.created; playSoundEffect(.sent) |
| 80 | +check("muting after playback prevents the next allocation", NSSound.created == prior) |
| 81 | +reset(); systemMutes[0] = 1; playSoundEffectBeep() |
| 82 | +check("incorrect-answer system beep respects mute", NSSound.played == 0) |
| 83 | +reset(); playSoundEffectBeep() |
| 84 | +check("audible incorrect answer retains the system beep", NSSound.played == 1) |
| 85 | +reset(); mutes[0] = 1; playSoundEffectBeep() |
| 86 | +check("muted ordinary output does not mute a separate audible alert device", NSSound.played == 1) |
| 87 | +reset(); systemMutes[0] = 1; playSoundEffect(.sent); playSoundEffectBeep() |
| 88 | +check("muted alert device blocks beep while ordinary output stays audible", NSSound.created == 1 && NSSound.played == 1) |
| 89 | +reset(); NSSound.createdHook = { uid = "new-default" }; playSoundEffect(.sent) |
| 90 | +check("switching output before play follows the new default", NSSound.lastOutput == "new-default" && NSSound.lastPinnedOutput == nil) |
| 91 | +reset(); NSSound.createdHook = { device = nil; uid = nil }; playSoundEffect(.sent) |
| 92 | +check("disconnecting the output before playback suppresses the sound", NSSound.played == 0) |
| 93 | +reset(); NSSound.createdHook = { mutes[0] = 1 }; playSoundEffect(.sent) |
| 94 | +check("muting the new output before playback is rechecked", NSSound.played == 0) |
| 95 | +print("\(checks - failures)/\(checks) checks passed") |
| 96 | +exit(failures == 0 ? 0 : 1) |
0 commit comments