fix(macos): prevent Talk Mode audio hang
This commit is contained in:
79
apps/macos/Tests/ClawdisIPCTests/TalkAudioPlayerTests.swift
Normal file
79
apps/macos/Tests/ClawdisIPCTests/TalkAudioPlayerTests.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Clawdis
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
mutating func appendLEUInt16(_ value: UInt16) {
|
||||
var v = value.littleEndian
|
||||
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
||||
}
|
||||
|
||||
mutating func appendLEUInt32(_ value: UInt32) {
|
||||
var v = value.littleEndian
|
||||
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user