31f5937a7f
Reduce extension memory pressure and delivery races while adding richer candidates, tactile feedback, and safer two-level creative polishing.
78 lines
3.1 KiB
Swift
78 lines
3.1 KiB
Swift
// ASRConversionTests.swift
|
|
// OSGKeyboard · Tests
|
|
//
|
|
// Locks in the Float32→Int16 PCM conversion that `SpeechAnalyzerASR`
|
|
// runs on the audio thread. The dictation transcriber's precondition
|
|
// (`"Audio sample data must be 16-bit signed integers"`) trips if
|
|
// the conversion is wrong, so the scaling + clipping math here is
|
|
// the difference between "Speech works" and "Speech crashes" — worth
|
|
// a regression test even though it's only ~3 lines of arithmetic.
|
|
|
|
import XCTest
|
|
@testable import OSGKeyboardShared
|
|
@testable import OSGKeyboardHostSupport
|
|
|
|
final class ASRConversionTests: XCTestCase {
|
|
|
|
/// Edge cases: silence, full-scale positive, full-scale negative,
|
|
/// mid-scale positive, mid-scale negative, and the "above unity"
|
|
/// gain-overflow case. Each is the one number we'd most regret
|
|
/// getting wrong.
|
|
func testFloat32ToInt16EdgeCases() {
|
|
runConversion(
|
|
input: [0.0, 1.0, -1.0, 0.5, -0.5, 1.5, -1.5],
|
|
expected: [0, 32767, -32767, 16384, -16384, 32767, -32768]
|
|
)
|
|
}
|
|
|
|
/// Round-trip-ish: every Int16 in a small range should be
|
|
/// reachable from a corresponding Float input. Locks the
|
|
/// quantization step (1/32767) so a future "use a different
|
|
/// scaling" change has to update this test.
|
|
func testFloat32ToInt16RoundTrip() {
|
|
var input = [Float]()
|
|
var expected = [Int16]()
|
|
for i in stride(from: -32768, through: 32767, by: 1024) {
|
|
// Map Int16 back to its canonical Float source value:
|
|
// src = i / 32767.0 (so src=1.0 → i=32767, src=-1.0 → i=-32767).
|
|
// We don't test i=-32768 because the asymmetric range
|
|
// (Int16 is -32768...32767) means there is no Float
|
|
// that decodes back to exactly -32768.
|
|
let src = Float(i) / 32767.0
|
|
input.append(src)
|
|
expected.append(Int16(i))
|
|
}
|
|
runConversion(input: input, expected: expected)
|
|
}
|
|
|
|
/// Empty input must be a no-op. Guards against off-by-one in
|
|
/// the loop and against `UnsafePointer` access on a zero-length
|
|
/// array (which is undefined behaviour in C but valid in Swift).
|
|
func testFloat32ToInt16Empty() {
|
|
// `sourceCount == 0` with `nil` pointers is a no-op. The
|
|
// function guards on `sourceCount > 0` before dereferencing
|
|
// anything, so the nil pointers are safe.
|
|
ASRServiceFactory.convertFloat32ToInt16(
|
|
source: nil, sourceCount: 0, destination: nil
|
|
)
|
|
// Reaching here without crashing is the assertion.
|
|
}
|
|
|
|
// MARK: - Helper
|
|
|
|
private func runConversion(input: [Float], expected: [Int16]) {
|
|
precondition(input.count == expected.count, "test setup")
|
|
var actual = [Int16](repeating: 0, count: expected.count)
|
|
input.withUnsafeBufferPointer { src in
|
|
actual.withUnsafeMutableBufferPointer { dst in
|
|
ASRServiceFactory.convertFloat32ToInt16(
|
|
source: src.baseAddress,
|
|
sourceCount: src.count,
|
|
destination: dst.baseAddress
|
|
)
|
|
}
|
|
}
|
|
XCTAssertEqual(actual, expected)
|
|
}
|
|
}
|