Files
openclaw/apps/macos/Tests/OpenClawIPCTests/TalkBufferedAudioPlayerTests.swift
Peter Steinberger 4aa8bf12e3 refactor(voice): dedupe talk audio primitives into OpenClawKit (#103099)
* refactor(voice): dedupe talk audio primitives into OpenClawKit

One buffered TTS clip player (watchdog + metering) shared by iOS and
macOS replaces the near-identical pair; macOS TalkPlaybackResult was a
field-for-field duplicate of StreamingPlaybackResult and is gone. One
AVAudioPCMBuffer RMS helper replaces five per-file copies; the metered
PCM passthrough moves into PCMPlaybackEnvelope; TalkModeManager's
duplicated ElevenLabs PCM-then-mp3-retry blocks collapse into one
helper. Net -378 lines, no behavior change.

* fix(ios): drop stray blank line and resync i18n inventory

* fix(ios): drop stray blank line after helper extraction
2026-07-09 22:21:00 +01:00

99 lines
2.9 KiB
Swift

import Foundation
import OpenClawKit
import Testing
@testable import OpenClaw
@Suite(.serialized) struct TalkBufferedAudioPlayerTests {
@MainActor
@Test func `play does not hang when playback ends or fails`() async throws {
let wav = makeWav16Mono(sampleRate: 8000, samples: 80)
defer { _ = TalkBufferedAudioPlayer.shared.stop() }
_ = try await withTimeout(seconds: 10.0) {
await TalkBufferedAudioPlayer.shared.play(data: wav)
}
#expect(true)
}
@MainActor
@Test func `play does not hang when play is called twice`() async throws {
let wav = makeWav16Mono(sampleRate: 8000, samples: 800)
defer { _ = TalkBufferedAudioPlayer.shared.stop() }
let first = Task { @MainActor in
await TalkBufferedAudioPlayer.shared.play(data: wav)
}
await Task.yield()
_ = await TalkBufferedAudioPlayer.shared.play(data: wav)
_ = try await withTimeout(seconds: 10.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) }
}
}