Files
clawdbot/apps/macos/Tests/ClawdbotIPCTests/TalkAudioPlayerTests.swift
2026-01-08 02:36:29 +00:00

98 lines
2.9 KiB
Swift

import Foundation
import Testing
@testable import Clawdbot
@Suite(.serialized) struct TalkAudioPlayerTests {
@MainActor
@Test func playDoesNotHangWhenPlaybackEndsOrFails() async throws {
let wav = makeWav16Mono(sampleRate: 8000, samples: 80)
defer { _ = TalkAudioPlayer.shared.stop() }
_ = try await withTimeout(seconds: 2.0) {
await TalkAudioPlayer.shared.play(data: wav)
}
#expect(true)
}
@MainActor
@Test func playDoesNotHangWhenPlayIsCalledTwice() async throws {
let wav = makeWav16Mono(sampleRate: 8000, samples: 800)
defer { _ = TalkAudioPlayer.shared.stop() }
let first = Task { @MainActor in
await TalkAudioPlayer.shared.play(data: wav)
}
await Task.yield()
_ = await TalkAudioPlayer.shared.play(data: wav)
_ = try await withTimeout(seconds: 2.0) {
await first.value
}
#expect(true)
}
}
private struct TimeoutError: Error {}
private func withTimeout<T: Sendable>(
seconds: Double,
_ work: @escaping @Sendable () async throws -> T) async throws -> T
{
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await work()
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw TimeoutError()
}
let result = try await group.next()
group.cancelAll()
guard let result else { throw TimeoutError() }
return result
}
}
private func makeWav16Mono(sampleRate: UInt32, samples: Int) -> Data {
let channels: UInt16 = 1
let bitsPerSample: UInt16 = 16
let blockAlign = channels * (bitsPerSample / 8)
let byteRate = sampleRate * UInt32(blockAlign)
let dataSize = UInt32(samples) * UInt32(blockAlign)
var data = Data()
data.append(contentsOf: [0x52, 0x49, 0x46, 0x46]) // RIFF
data.appendLEUInt32(36 + dataSize)
data.append(contentsOf: [0x57, 0x41, 0x56, 0x45]) // WAVE
data.append(contentsOf: [0x66, 0x6D, 0x74, 0x20]) // fmt
data.appendLEUInt32(16) // PCM
data.appendLEUInt16(1) // audioFormat
data.appendLEUInt16(channels)
data.appendLEUInt32(sampleRate)
data.appendLEUInt32(byteRate)
data.appendLEUInt16(blockAlign)
data.appendLEUInt16(bitsPerSample)
data.append(contentsOf: [0x64, 0x61, 0x74, 0x61]) // data
data.appendLEUInt32(dataSize)
// Silence samples.
data.append(Data(repeating: 0, count: Int(dataSize)))
return data
}
extension Data {
fileprivate mutating func appendLEUInt16(_ value: UInt16) {
var v = value.littleEndian
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
}
fileprivate mutating func appendLEUInt32(_ value: UInt32) {
var v = value.littleEndian
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
}
}