Skip to content

Commit 06d68e2

Browse files
committed
Working towards cross-platform compatability by implementing vDSP functions in plain Swift
1 parent a7c5dfd commit 06d68e2

9 files changed

Lines changed: 268 additions & 54 deletions

File tree

Sources/SignalTools/Correlation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public func slidingCorrelation(signal: [Float], template: [Float]) -> [Float]? {
1717
template.withUnsafeBufferPointer { templatePtr in
1818
let signalBasePointer = signalPtr.baseAddress!
1919
let templateBasePointer = templatePtr.baseAddress!
20-
vDSP_conv(signalBasePointer, vDSP_Stride(1), templateBasePointer, vDSP_Stride(1), &result, vDSP_Stride(1), vDSP_Length(outputCount), vDSP_Length(template.count))
20+
DSP.convolve(signalBasePointer, 1, templateBasePointer, 1, &result, 1, outputCount, template.count)
2121
}
2222
}
2323
return result

Sources/SignalTools/DSPBackend.swift

Lines changed: 220 additions & 15 deletions
Large diffs are not rendered by default.

Sources/SignalTools/DSPTypes.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,36 @@ import Foundation
99

1010
#if canImport(Accelerate)
1111
import Accelerate
12-
public typealias ComplexSignal = DSPComplex
13-
public typealias SplitComplexSignal = DSPSplitComplex
12+
public typealias ComplexSample = DSPComplex
13+
public typealias SplitComplexSamples = DSPSplitComplex
1414
#else
1515
struct ComplexSignal: Equatable {
1616
var real: Float
1717
var imag: Float
1818
}
1919

2020
struct SplitComplexSignal {
21-
var real: UnsafeMutablePointer<Float>
22-
var imag: UnsafeMutablePointer<Float>
21+
var realp: UnsafeMutablePointer<Float>
22+
var imagp: UnsafeMutablePointer<Float>
2323
}
2424
#endif
2525

26-
public extension ComplexSignal {
26+
public extension ComplexSample {
2727
func magnitude() -> Float {
2828
return ((real * real) + (imag * imag)).squareRoot()
2929
}
30+
31+
func conjugate() -> ComplexSample {
32+
return ComplexSample(real: real, imag: -imag)
33+
}
3034
}
3135

32-
public extension [ComplexSignal] {
36+
public extension [ComplexSample] {
3337
func magnitude() -> [Float] {
34-
return self.map({$0.magnitude()})
38+
return self.map { $0.magnitude() }
39+
}
40+
41+
func conjugate() -> [ComplexSample] {
42+
return self.map { $0.conjugate() }
3543
}
3644
}

Sources/SignalTools/Demodulation/Analog/FM.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
/// This function will do FM demodulation, but much slower than demodulateFM
1212
/// Here for conceptual understanding.
13-
public func demodulateFMSlow(_ samples: [DSPComplex]) -> [Float] {
13+
public func demodulateFMSlow(_ samples: [ComplexSample]) -> [Float] {
1414
var diffs = [Float].init(repeating: 0.0, count: samples.count - 1)
1515
for i in 1..<samples.count {
1616
let i0 = samples[i-1].real
@@ -26,8 +26,8 @@ public func demodulateFMSlow(_ samples: [DSPComplex]) -> [Float] {
2626
}
2727

2828
/// Demodulates FM by mutliplying each sample by the conjugate of the preceding sample, providing phase in radians.
29-
public func demodulateFM(_ samples: [DSPComplex]) -> [Float] {
30-
let n = vDSP_Length(samples.count - 1)
29+
public func demodulateFM(_ samples: [ComplexSample]) -> [Float] {
30+
let n = samples.count - 1
3131
var diffs = [Float].init(repeating: 0.0, count: samples.count - 1)
3232
samples.withUnsafeBufferPointer { samplesPtr in
3333
let basePointer = samplesPtr.baseAddress!
@@ -39,15 +39,15 @@ public func demodulateFM(_ samples: [DSPComplex]) -> [Float] {
3939
var tempReal = [Float].init(repeating: 0.0, count: samples.count - 1)
4040
var tempIm = [Float].init(repeating: 0.0, count: samples.count - 1)
4141

42-
let stride = vDSP_Stride(2) // One IQSample struct's worth of memory should be 2 floats
43-
let shortStride = vDSP_Stride(1)
42+
let stride = 2
43+
let shortStride = 1
4444
tempReal.withUnsafeMutableBufferPointer { tempRealPtr in
4545
tempIm.withUnsafeMutableBufferPointer { tempImPtr in
46-
var A: DSPSplitComplex = .init(realp: UnsafeMutablePointer(mutating: i0), imagp: UnsafeMutablePointer(mutating: q0)) // prev
47-
var B: DSPSplitComplex = .init(realp: UnsafeMutablePointer(mutating: i1), imagp: UnsafeMutablePointer(mutating: q1)) // curr
48-
var C: DSPSplitComplex = .init(realp: tempRealPtr.baseAddress!, imagp: tempImPtr.baseAddress!)
49-
vDSP_zvmul(&A, stride, &B, stride, &C, 1, n, -1)
50-
vDSP_zvphas(&C, shortStride, &diffs, shortStride, n)
46+
var A: SplitComplexSamples = .init(realp: UnsafeMutablePointer(mutating: i0), imagp: UnsafeMutablePointer(mutating: q0)) // prev
47+
var B: SplitComplexSamples = .init(realp: UnsafeMutablePointer(mutating: i1), imagp: UnsafeMutablePointer(mutating: q1)) // curr
48+
var C: SplitComplexSamples = .init(realp: tempRealPtr.baseAddress!, imagp: tempImPtr.baseAddress!)
49+
DSP.multiplyComplexVectors(&A, stride, &B, stride, &C, 1, n, true)
50+
DSP.phase(&C, shortStride, &diffs, shortStride, n)
5151
}
5252
}
5353
}

Sources/SignalTools/Downsample.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
//
55
// Created by Connor Gibbons on 6/24/25.
66
//
7+
import Foundation
78

89
public class Downsampler {
910
private var decimationFactor: Int
1011
private var filter: [Float]
1112

1213
// State
1314
private var realContext: [Float]
14-
private var complexContext: [DSPComplex]
15+
private var complexContext: [ComplexSample]
1516
private var realSkipCount: Int
1617
private var complexSkipCount: Int
1718

Sources/SignalTools/File Utilities/Read/wav.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import Foundation
1111
/// Read IQ Samples from a .wav where samples are stored as 16-bit integers.
1212
/// Samples are expected to be interleaved IQ.
1313
/// Samples are adjusted to be in range [-1,1]
14-
public func readIQFromWAV16Bit(fileURL: URL) throws -> [DSPComplex] {
14+
public func readIQFromWAV16Bit(fileURL: URL) throws -> [ComplexSample] {
1515
let data = try Data(contentsOf: fileURL)
1616

17-
var iqOutput: [DSPComplex] = []
17+
var iqOutput: [ComplexSample] = []
1818

1919
let iqData = data.dropFirst(44)
2020
guard iqData.count % 4 == 0 else {
@@ -28,14 +28,14 @@ public func readIQFromWAV16Bit(fileURL: URL) throws -> [DSPComplex] {
2828
while currOffset < int16ArrayBasePointer.count {
2929
let realSample = Float(int16ArrayBasePointer[currOffset]) / 32768.0
3030
let imagSample = Float(int16ArrayBasePointer[currOffset + 1]) / 32768.0
31-
iqOutput.append(DSPComplex(real: realSample, imag: imagSample))
31+
iqOutput.append(ComplexSample(real: realSample, imag: imagSample))
3232
currOffset += 2
3333
}
3434
}
3535

3636
return iqOutput
3737
}
38-
public func readIQFromWAV16Bit(filePath: String) throws -> [DSPComplex] {
38+
public func readIQFromWAV16Bit(filePath: String) throws -> [ComplexSample] {
3939
let fileURL = URL(fileURLWithPath: filePath)
4040
return try readIQFromWAV16Bit(fileURL: fileURL)
4141
}

Sources/SignalTools/File Utilities/Write/csv.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public func writeAudioToFile(_ audio: [Float], path: String = "") {
5151

5252
/// Write samples to specified path in .csv format.
5353
/// Each line is as follows: I,Q\n
54-
public func samplesToCSV(_ samples: [ComplexSignal], path: String) {
54+
public func samplesToCSV(_ samples: [ComplexSample], path: String) {
5555
var csvText = "I,Q\n"
5656
for sample in samples {
5757
csvText.append("\(sample.real),\(sample.imag)\n")

Sources/SignalTools/Filter.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
public protocol Filter {
99
func filteredSignal( _ input: inout [Float])
10-
func filteredSignal(_ input: inout [ComplexSignal])
10+
func filteredSignal(_ input: inout [ComplexSample])
1111
}
1212

1313
public enum FilterType {
@@ -48,7 +48,7 @@ public class IIRFilter: Filter {
4848
biquad!.apply(input: input, output: &input)
4949
}
5050

51-
public func filteredSignal(_ input: inout [ComplexSignal]) {
51+
public func filteredSignal(_ input: inout [ComplexSample]) {
5252
if biquad == nil {
5353
initBiquad()
5454
}
@@ -90,7 +90,7 @@ public class FIRFilter: Filter {
9090
var taps: [Float]
9191
var tapsLength: Int
9292
var stateBuffer: UnsafeMutableBufferPointer<Float> // Last 'tapsLength - 1' values from previous buffer, need for convolution
93-
var complexStateBuffer: UnsafeMutableBufferPointer<ComplexSignal>
93+
var complexStateBuffer: UnsafeMutableBufferPointer<ComplexSample>
9494

9595
public init(type: FilterType, cutoffFrequency: Double, sampleRate: Int, tapsLength: Int, windowFunc: vDSP.WindowSequence = .hamming) throws {
9696
var generatedFilter: [Float]
@@ -104,7 +104,7 @@ public class FIRFilter: Filter {
104104
stateBuffer = .allocate(capacity: tapsLength - 1)
105105
stateBuffer.initialize(repeating: 0.0)
106106
complexStateBuffer = .allocate(capacity: tapsLength - 1)
107-
complexStateBuffer.initialize(repeating: ComplexSignal(real: 0, imag: 0))
107+
complexStateBuffer.initialize(repeating: ComplexSample(real: 0, imag: 0))
108108
}
109109

110110
public init(taps: [Float]) {
@@ -113,7 +113,7 @@ public class FIRFilter: Filter {
113113
stateBuffer = .allocate(capacity: tapsLength - 1)
114114
stateBuffer.initialize(repeating: 0.0)
115115
complexStateBuffer = .allocate(capacity: tapsLength - 1)
116-
complexStateBuffer.initialize(repeating: ComplexSignal(real: 0, imag: 0))
116+
complexStateBuffer.initialize(repeating: ComplexSample(real: 0, imag: 0))
117117
}
118118

119119
deinit {
@@ -137,8 +137,8 @@ public class FIRFilter: Filter {
137137
input = tempOutputBuffer
138138
}
139139

140-
public func filteredSignal(_ input: inout [ComplexSignal]) {
141-
let workingBuffer = UnsafeMutableBufferPointer<ComplexSignal>.allocate(capacity: input.count + tapsLength - 1)
140+
public func filteredSignal(_ input: inout [ComplexSample]) {
141+
let workingBuffer = UnsafeMutableBufferPointer<ComplexSample>.allocate(capacity: input.count + tapsLength - 1)
142142
defer {
143143
workingBuffer.deallocate()
144144
}
@@ -166,9 +166,9 @@ public class FIRFilter: Filter {
166166
vDSP.convert(splitComplexVector: splitComplexOutputBuffer, toInterleavedComplexVector: &input)
167167
}
168168

169-
public func filtfilt(_ input: inout [ComplexSignal]) {
169+
public func filtfilt(_ input: inout [ComplexSample]) {
170170
self.filteredSignal(&input)
171-
var reversedFilteredSignal: [ComplexSignal] = input.reversed()
171+
var reversedFilteredSignal: [ComplexSample] = input.reversed()
172172
let freshFilter = FIRFilter(taps: self.taps)
173173
freshFilter.filteredSignal(&reversedFilteredSignal)
174174
input = reversedFilteredSignal.reversed()
@@ -191,7 +191,7 @@ public class FIRFilter: Filter {
191191
_ = stateBuffer.update(fromContentsOf: input.dropFirst(input.count - tapsLength + 1))
192192
}
193193

194-
private func copyToComplexStateBuffer(_ input: inout [ComplexSignal]) {
194+
private func copyToComplexStateBuffer(_ input: inout [ComplexSample]) {
195195
_ = complexStateBuffer.update(fromContentsOf: input.dropFirst(input.count - tapsLength + 1))
196196
}
197197

Sources/SignalTools/Utils.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77

88
// Frequency Shifting
99

10-
public func shiftFrequencyToBaseband(rawIQ: [ComplexSignal], result: inout [ComplexSignal], frequency: Float, sampleRate: Int) {
10+
public func shiftFrequencyToBaseband(rawIQ: [ComplexSample], result: inout [ComplexSample], frequency: Float, sampleRate: Int) {
1111
guard rawIQ.count == result.count else {
1212
return
1313
}
1414

1515
let sampleCount = rawIQ.count
1616
let complexMixerArray = (0..<sampleCount).map{ index in
1717
let angle = -2 * Float.pi * frequency * Float(index) / Float(sampleRate)
18-
return ComplexSignal(real: cos(angle), imag: sin(angle))
18+
return ComplexSample(real: cos(angle), imag: sin(angle))
1919
}
2020

2121
var splitInputBuffer = DSPSplitComplex(realp: .allocate(capacity: sampleCount), imagp: .allocate(capacity: sampleCount))
@@ -38,7 +38,7 @@ public func shiftFrequencyToBaseband(rawIQ: [ComplexSignal], result: inout [Comp
3838

3939
/// I've found that without using Double internally, weird artifacts can occur.
4040
/// Fair warning, this is probably a lot slower than the regular shift to baseband function.
41-
public func shiftFrequencyToBasebandHighPrecision(rawIQ: [ComplexSignal], result: inout [ComplexSignal], frequency: Float, sampleRate: Int) {
41+
public func shiftFrequencyToBasebandHighPrecision(rawIQ: [ComplexSample], result: inout [ComplexSample], frequency: Float, sampleRate: Int) {
4242
guard rawIQ.count == result.count else {
4343
return
4444
}
@@ -101,7 +101,7 @@ public func radToFrequency(radDiffs: [Float], sampleRate: Int) -> [Float] {
101101
}
102102

103103
/// Calculates angle (radians) for each entry in an array of IQ samples.
104-
public func calculateAngle(rawIQ: [ComplexSignal], result: inout [Float]) {
104+
public func calculateAngle(rawIQ: [ComplexSample], result: inout [Float]) {
105105
let sampleCount = rawIQ.count
106106
guard sampleCount == result.count && !rawIQ.isEmpty else {
107107
return
@@ -179,7 +179,7 @@ public func valsAreClose(_ arr1: [Float], _ arr2: [Float], threshold: Float = 0.
179179
}
180180

181181
/// Determines if all elements at the same index across two arrays are within a provided threshold.
182-
public func valsAreClose(_ arr1: [ComplexSignal],_ arr2: [ComplexSignal], threshold: Float = 0.001) -> Bool {
182+
public func valsAreClose(_ arr1: [ComplexSample],_ arr2: [ComplexSample], threshold: Float = 0.001) -> Bool {
183183
guard arr1.count == arr2.count else { return false }
184184
for i in 0..<arr1.count {
185185
if abs(arr1[i].imag - arr2[i].imag) > threshold {

0 commit comments

Comments
 (0)