Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions Sources/FluidAudio/ASR/Paraformer/ParaformerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<count {
var r = [Float](repeating: 0, count: dim)
let base = t * frameStride
for d in 0..<dim { r[d] = Float(p[base + d]) }
r.withUnsafeMutableBufferPointer { dst in
Float16Conversion.toFloat32(
src: p + t * frameStride, dst: dst.baseAddress!, count: dim)
}
out.append(r)
}
}
Expand Down
18 changes: 4 additions & 14 deletions Sources/FluidAudio/ASR/Shared/LogitsArgmax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,12 @@ enum LogitsArgmax {
run(logits.dataPointer.assumingMemoryBound(to: Float32.self))
} else {
let count = frames * frameStride
let src = logits.dataPointer.assumingMemoryBound(to: Float16.self)
// Address fp16 storage as raw bit patterns: Swift's `Float16` is
// unavailable on macOS x86_64.
let src = logits.dataPointer.assumingMemoryBound(to: UInt16.self)
var buf = [Float](repeating: 0, count: count)
// The vImage buffers must not outlive the pointers they wrap, so the
// convert + argmax both run inside the withUnsafe scope.
buf.withUnsafeMutableBufferPointer { dst in
var srcBuf = vImage_Buffer(
data: UnsafeMutableRawPointer(mutating: src),
height: 1,
width: vImagePixelCount(count),
rowBytes: count * MemoryLayout<Float16>.size)
var dstBuf = vImage_Buffer(
data: dst.baseAddress!,
height: 1,
width: vImagePixelCount(count),
rowBytes: count * MemoryLayout<Float>.size)
vImageConvert_Planar16FtoPlanarF(&srcBuf, &dstBuf, 0)
Float16Conversion.toFloat32(src: src, dst: dst.baseAddress!, count: count)
run(dst.baseAddress!)
}
}
Expand Down
37 changes: 37 additions & 0 deletions Sources/FluidAudio/Shared/Float16Conversion.swift
Original file line number Diff line number Diff line change
@@ -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<UInt16>, dst: UnsafeMutablePointer<Float>, count: Int
) {
var srcBuf = vImage_Buffer(
data: UnsafeMutableRawPointer(mutating: src), height: 1,
width: vImagePixelCount(count),
rowBytes: count * MemoryLayout<UInt16>.stride)
var dstBuf = vImage_Buffer(
data: dst, height: 1, width: vImagePixelCount(count),
rowBytes: count * MemoryLayout<Float>.stride)
vImageConvert_Planar16FtoPlanarF(&srcBuf, &dstBuf, 0)
}

/// Narrow `count` Float32 values to half precision (as raw bit patterns).
static func fromFloat32(
src: UnsafePointer<Float>, dst: UnsafeMutablePointer<UInt16>, count: Int
) {
var srcBuf = vImage_Buffer(
data: UnsafeMutableRawPointer(mutating: src), height: 1,
width: vImagePixelCount(count),
rowBytes: count * MemoryLayout<Float>.stride)
var dstBuf = vImage_Buffer(
data: dst, height: 1, width: vImagePixelCount(count),
rowBytes: count * MemoryLayout<UInt16>.stride)
vImageConvert_PlanarFtoPlanar16F(&srcBuf, &dstBuf, 0)
}
}
14 changes: 8 additions & 6 deletions Sources/FluidAudio/TTS/NeuTts/NeuTtsSynthesizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<layerElements {
dst[i] = Float16(base[i])
}
// Address fp16 storage as raw bit patterns: Swift's
// `Float16` is unavailable on macOS x86_64.
let dst = dstBase.assumingMemoryBound(to: UInt16.self)
Float16Conversion.fromFloat32(src: base, dst: dst, count: layerElements)
}
}
}
Expand Down Expand Up @@ -227,8 +227,10 @@ struct NeuTtsSynthesizer {
dst.baseAddress!.update(from: ptr, count: count)
}
case .float16:
let ptr = array.dataPointer.assumingMemoryBound(to: Float16.self)
for i in 0..<count { out[i] = Float(ptr[i]) }
let ptr = array.dataPointer.assumingMemoryBound(to: UInt16.self)
out.withUnsafeMutableBufferPointer { dst in
Float16Conversion.toFloat32(src: ptr, dst: dst.baseAddress!, count: count)
}
default:
throw SynthesisError.missingOutput("unexpected dataType \(array.dataType.rawValue)")
}
Expand Down
61 changes: 61 additions & 0 deletions Tests/FluidAudioTests/Shared/Float16ConversionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import XCTest

@testable import FluidAudio

/// Uses hard-coded IEEE-754 binary16 bit patterns (not `Float16`) so the
/// test itself compiles on macOS x86_64.
final class Float16ConversionTests: XCTestCase {

private let cases: [(bits: UInt16, value: Float)] = [
(0x0000, 0.0),
(0x3800, 0.5),
(0x3C00, 1.0),
(0xC000, -2.0),
(0x4248, 3.140625),
(0x7BFF, 65504.0), // largest finite half
(0x0001, Float(5.960464477539063e-08)), // smallest subnormal
]

func testToFloat32KnownBitPatterns() {
let src = cases.map { $0.bits }
var dst = [Float](repeating: .nan, count: src.count)
src.withUnsafeBufferPointer { s in
dst.withUnsafeMutableBufferPointer { d in
Float16Conversion.toFloat32(src: s.baseAddress!, dst: d.baseAddress!, count: s.count)
}
}
for (i, expected) in cases.map({ $0.value }).enumerated() {
XCTAssertEqual(dst[i], expected, "index \(i)")
}
}

func testFromFloat32KnownBitPatterns() {
let src = cases.map { $0.value }
var dst = [UInt16](repeating: 0xFFFF, count: src.count)
src.withUnsafeBufferPointer { s in
dst.withUnsafeMutableBufferPointer { d in
Float16Conversion.fromFloat32(src: s.baseAddress!, dst: d.baseAddress!, count: s.count)
}
}
for (i, expected) in cases.map({ $0.bits }).enumerated() {
XCTAssertEqual(dst[i], expected, "index \(i)")
}
}

func testRoundTripIsExactForHalfRepresentableValues() {
let values: [Float] = [0.0, -0.125, 0.25, 1.5, -3.0, 1024.0, -65504.0]
var bits = [UInt16](repeating: 0, count: values.count)
var back = [Float](repeating: .nan, count: values.count)
values.withUnsafeBufferPointer { s in
bits.withUnsafeMutableBufferPointer { d in
Float16Conversion.fromFloat32(src: s.baseAddress!, dst: d.baseAddress!, count: s.count)
}
}
bits.withUnsafeBufferPointer { s in
back.withUnsafeMutableBufferPointer { d in
Float16Conversion.toFloat32(src: s.baseAddress!, dst: d.baseAddress!, count: s.count)
}
}
XCTAssertEqual(back, values)
}
}
Loading