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..