From 4c17f8d53f6bf4bc5956f7759a2261791af20990 Mon Sep 17 00:00:00 2001 From: Alex-Wengg Date: Sat, 22 Aug 2026 19:26:00 -0400 Subject: [PATCH] fix(build): restore macOS x86_64 compile by removing unguarded Float16 (#875) v0.15.6 added Paraformer, LogitsArgmax, and NeuTTS code that addresses fp16 MLMultiArray storage via Swift.Float16, which is unavailable on macOS x86_64. This broke Intel swift build and downstream universal (arm64 + x86_64) archives even for apps not using those features, since the sources are part of the monolithic FluidAudio target. Replace the Float16-typed accesses with the established portable pattern already used by KokoroAneArrays and ANEMemoryUtils: address the half-precision storage as UInt16 bit patterns and convert with vImage's Planar16F routines, now factored into a shared Float16Conversion enum. The NeuTTS seedState and floatBuffer scalar loops become vectorized vImage passes as a side effect; results are bit-identical. Also add an x86_64 cross-compile job to tests.yml so unguarded Float16 usage fails PR CI instead of surfacing on Intel machines after release. Verified: swift build --arch x86_64 reproduces all errors from the issue on unpatched main and passes with this change; native arm64 build unchanged. --- .github/workflows/tests.yml | 12 ++++ .../ASR/Paraformer/ParaformerManager.swift | 10 ++- .../FluidAudio/ASR/Shared/LogitsArgmax.swift | 18 ++---- .../FluidAudio/Shared/Float16Conversion.swift | 37 +++++++++++ .../TTS/NeuTts/NeuTtsSynthesizer.swift | 14 +++-- .../Shared/Float16ConversionTests.swift | 61 +++++++++++++++++++ 6 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 Sources/FluidAudio/Shared/Float16Conversion.swift create mode 100644 Tests/FluidAudioTests/Shared/Float16ConversionTests.swift diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c85eb044..22585dc9e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,18 @@ jobs: run: swift test --parallel --num-workers $(sysctl -n hw.ncpu) timeout-minutes: 20 + build-macos-x86_64: + name: Build Swift Package (macOS x86_64 cross-compile) + runs-on: macos-15 + steps: + - name: Checkout code + uses: actions/checkout@v5 + + # Intel macs have no Swift Float16; this guards against unguarded + # fp16 usage breaking universal builds (issue #875). + - name: Build package (x86_64) + run: swift build --arch x86_64 + build-ios: name: Build (iOS) runs-on: macos-15 diff --git a/Sources/FluidAudio/ASR/Paraformer/ParaformerManager.swift b/Sources/FluidAudio/ASR/Paraformer/ParaformerManager.swift index 3f0706eee..c0cfc14bf 100644 --- a/Sources/FluidAudio/ASR/Paraformer/ParaformerManager.swift +++ b/Sources/FluidAudio/ASR/Paraformer/ParaformerManager.swift @@ -474,11 +474,15 @@ public actor ParaformerManager { out.append(Array(UnsafeBufferPointer(start: p + t * frameStride, count: dim))) } } else { - let p = arr.dataPointer.assumingMemoryBound(to: Float16.self) + // Address fp16 storage as raw bit patterns: Swift's `Float16` is + // unavailable on macOS x86_64. + let p = arr.dataPointer.assumingMemoryBound(to: UInt16.self) for t in 0...size) - var dstBuf = vImage_Buffer( - data: dst.baseAddress!, - height: 1, - width: vImagePixelCount(count), - rowBytes: count * MemoryLayout.size) - vImageConvert_Planar16FtoPlanarF(&srcBuf, &dstBuf, 0) + Float16Conversion.toFloat32(src: src, dst: dst.baseAddress!, count: count) run(dst.baseAddress!) } } diff --git a/Sources/FluidAudio/Shared/Float16Conversion.swift b/Sources/FluidAudio/Shared/Float16Conversion.swift new file mode 100644 index 000000000..6ffdc6529 --- /dev/null +++ b/Sources/FluidAudio/Shared/Float16Conversion.swift @@ -0,0 +1,37 @@ +import Accelerate + +/// Portable fp16 ↔ fp32 buffer conversions. +/// +/// Swift's `Float16` is unavailable on macOS x86_64, so half-precision +/// buffers are addressed as `UInt16` bit patterns and converted with +/// vImage's Planar16F routines, which compile on every architecture. +enum Float16Conversion { + + /// Widen `count` half-precision values (as raw bit patterns) to Float32. + static func toFloat32( + src: UnsafePointer, dst: UnsafeMutablePointer, count: Int + ) { + var srcBuf = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: src), height: 1, + width: vImagePixelCount(count), + rowBytes: count * MemoryLayout.stride) + var dstBuf = vImage_Buffer( + data: dst, height: 1, width: vImagePixelCount(count), + rowBytes: count * MemoryLayout.stride) + vImageConvert_Planar16FtoPlanarF(&srcBuf, &dstBuf, 0) + } + + /// Narrow `count` Float32 values to half precision (as raw bit patterns). + static func fromFloat32( + src: UnsafePointer, dst: UnsafeMutablePointer, count: Int + ) { + var srcBuf = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: src), height: 1, + width: vImagePixelCount(count), + rowBytes: count * MemoryLayout.stride) + var dstBuf = vImage_Buffer( + data: dst, height: 1, width: vImagePixelCount(count), + rowBytes: count * MemoryLayout.stride) + vImageConvert_PlanarFtoPlanar16F(&srcBuf, &dstBuf, 0) + } +} diff --git a/Sources/FluidAudio/TTS/NeuTts/NeuTtsSynthesizer.swift b/Sources/FluidAudio/TTS/NeuTts/NeuTtsSynthesizer.swift index b5a7ed2b8..39114e2c7 100644 --- a/Sources/FluidAudio/TTS/NeuTts/NeuTtsSynthesizer.swift +++ b/Sources/FluidAudio/TTS/NeuTts/NeuTtsSynthesizer.swift @@ -167,10 +167,10 @@ struct NeuTtsSynthesizer { state.withMultiArray(for: name) { array in array.withUnsafeMutableBytes { rawBuffer, _ in guard let dstBase = rawBuffer.baseAddress else { return } - let dst = dstBase.assumingMemoryBound(to: Float16.self) - for i in 0..