Предисловие: Мальчик, просеивание всего этого заняло у меня почти 2 часа, и это все еще даже не идеально, но мальчик это намного приятнее.Надеюсь, это поможет!
Ваш код сильно страдает, потому что API-интерфейсы Accelerate - это API-интерфейсы C, которые не адаптированы для использования возможностей Swift.У вас будет гораздо более читабельный код, если вы сделаете себе несколько приятных обёрток для Accelerate API, который позволит вам убрать все «уродливые вещи» в угол, который вам редко приходится видеть или редактировать.
Я сделал этосоздав новый тип, ComplexFloatArray
, который похож на DSPSplitComplex
, но фактически инкапсулирует и владеет своими буферами.Это предотвращает висячие буферы, к которым DSPSplitComplex
подвержен.
После обработки типов ComplexFloatArray
пришло время определить некоторые оболочки для используемых вами функций ускорения.В этом случае vDSP_zvmul
и vDSP_fft_zop
.Поскольку в C нет кортежей, для возврата нескольких значений из функции C обычно требуется использование out-параметров, которые широко используются в инфраструктуре Accelerate.Мы можем перепроектировать их как функции Swift с обычными типами возврата.Эти API очень естественно выражены как методы экземпляра, которые работают с ComplexFloatArray
, поэтому мы разместим их там.
Кроме того, ваш код значительно усложнен из-за его зависимости от внешнего состояния.Свертка - это функция, нет причины, по которой она делает что-либо, кроме ввода данных (через параметры, а , а не через переменные экземпляра) и возвращает результат (через возвращаемое значение, а не черезпеременные экземпляра).
import Accelerate
class ComplexFloatArray {
var reals: [Float]
var imaginaries: [Float]
init(reals: [Float], imaginaries: [Float]) {
self.reals = reals
self.imaginaries = imaginaries
}
}
extension ComplexFloatArray { // Core features
var count: Int {
assert(reals.count == imaginaries.count)
return reals.count
}
static let stride = 1
func append(real: Float, imaginary: Float) {
self.reals.append(real)
self.imaginaries.append(imaginary)
}
func useAsDSPSplitComplex<R>(_ closure: (inout DSPSplitComplex) -> R) -> R {
return reals.withUnsafeMutableBufferPointer { realBufferPointer in
return imaginaries.withUnsafeMutableBufferPointer { imaginaryBufferPointer in
var dspSplitComplex = DSPSplitComplex(realp: realBufferPointer.baseAddress!, imagp: imaginaryBufferPointer.baseAddress!)
return closure(&dspSplitComplex)
}
}
}
}
extension ComplexFloatArray { // Convenience utilities
convenience init() {
self.init(reals: [], imaginaries: [])
}
static func zeros(count: Int) -> ComplexFloatArray {
return ComplexFloatArray(reals: Array(repeating: 0, count: count), imaginaries:Array(repeating: 0, count: count))
}
}
extension ComplexFloatArray { // Vector multiplciation extensions
enum ComplexMultiplicationType: Int32 { case normal = 1, conjugate = -1 }
func complexMultiply(
with other: ComplexFloatArray,
multiplicationType: ComplexMultiplicationType = .normal
) -> ComplexFloatArray {
assert(self.count == other.count, "Multiplied vectors must have the same size!")
let result = ComplexFloatArray.zeros(count: self.count)
self.useAsDSPSplitComplex { selfPointer in
other.useAsDSPSplitComplex { otherPointer in
result.useAsDSPSplitComplex { resultPointer in
vDSP_zvmul(
&selfPointer, ComplexFloatArray.stride,
&otherPointer, ComplexFloatArray.stride,
&resultPointer, ComplexFloatArray.stride, vDSP_Length(result.count),
multiplicationType.rawValue)
}
}
}
return result
}
}
extension ComplexFloatArray { // FFT extensions
enum FourierTransformDirection: Int32 { case forward = 1, inverse = -1 }
//TODO: name log2n label better
func outOfPlaceComplexFourierTransform(
setup: FFTSetup,
resultSize: Int,
log2n: UInt,
direction: FourierTransformDirection
) -> ComplexFloatArray {
let result = ComplexFloatArray.zeros(count: resultSize)
self.useAsDSPSplitComplex { selfPointer in
result.useAsDSPSplitComplex{ resultPointer in
vDSP_fft_zop(
setup,
&selfPointer, ComplexFloatArray.stride,
&resultPointer, ComplexFloatArray.stride,
log2n,
direction.rawValue
)
}
}
return result
}
}
extension FFTSetup {
enum FourierTransformRadix: Int32 {
case radix2 = 0, radix3 = 1, radix5 = 2
// Static let constants are only initialized once
// This function's intent to to make sure this enum stays in sync with the raw constants the Accelerate framework uses
static let assertRawValuesAreCorrect: Void = {
func assertRawValue(for actual: FourierTransformRadix, isEqualTo expected: Int) {
assert(actual.rawValue == expected, "\(actual) has a rawValue of \(actual.rawValue), but expected \(expected).")
}
assertRawValue(for: .radix2, isEqualTo: kFFTRadix2)
assertRawValue(for: .radix3, isEqualTo: kFFTRadix3)
assertRawValue(for: .radix5, isEqualTo: kFFTRadix5)
}()
}
init(log2n: Int, _ radix: FourierTransformRadix) {
_ = FourierTransformRadix.assertRawValuesAreCorrect
guard let setup = vDSP_create_fftsetup(vDSP_Length(log2n), FFTRadix(radix.rawValue)) else {
fatalError("vDSP_create_fftsetup(\(log2n), \(radix)) returned nil")
}
self = setup
}
}
struct NameMe {
// I don't know what this is, but if it can somehow be removed,
// the whole convolveInput method could be moved into an extension on ComplexFloatArray.
var fftFilterBank: [ComplexFloatArray]
func convolve(samples: ComplexFloatArray) -> [ComplexFloatArray] {
// TODO: rework reimplement this code to remove the DC from samples, and add it back in
// //Remove DC from FFT Signal
// re.remove(at: 0)
// im.remove(at: 0)
let fftlength: UInt = 10 // Todo: what is this, exactly?
let fft1Input = samples.outOfPlaceComplexFourierTransform( // Rename me to something better
setup: FFTSetup(log2n: 16, .radix2),
resultSize: samples.count,
log2n: fftlength,
direction: .forward
)
return self.fftFilterBank.map { kernel in kernel.complexMultiply(with: fft1Input) }
}
// Stub for compatibility with the old API. Deprecate it and move to the
// `convolve(samples: ComplexFloatArray) -> [ComplexFloatArray]` as soon as possible.
func convolveInput(realsamples: [Float], imagsamples: [Float]) -> [ComplexFloatArray] {
return self.convolve(samples: ComplexFloatArray(reals: realsamples, imaginaries: imagsamples))
}
}
У меня есть несколько заметок по пути
- Эта функция WAAAAAAAAAAAY слишком длинная.Если у вас есть функция длиной более 10 строк, есть довольно сильный индикатор того, что она становится слишком большой, много делает и может выиграть от разбивки на более простые шаги.
- У вас есть лотов избыточных переменных.Вам не нужно более 1 копии любого данного неизменного значения.У вас есть все эти разные имена, относящиеся к одной и той же вещи, которая просто усложняет вещи.Можно привести аргумент, что это может быть полезно, если новые имена имеют значение, но такие имена, как
x
, y
, re
, im
почти бесполезны в своих коммуникативных способностях и должны почти-всегда следует избегать. Массивы являются типами значений с Copy-on-Write.Вы можете сделать их копии, просто присвоив им новую переменную, поэтому код, подобный следующему:
var reals = [Float]()
var imags = [Float]()
for i in 0..<x.count{
reals.append(x[i])
imags.append(y[i])
}
является медленным и визуально громоздким.Это может быть просто: let (reals, imags) = (x, y)
.Но опять же, эти копии не нужны (как и x
и y
).Удалите их и просто используйте realsamples
и imagsamples
напрямую.
Когда вы часто передаете несколько фрагментов данных вместе, это очень убедительный признак того, что вы должны определить новыйсовокупный тип, чтобы обернуть их.Например, если вы передаете два Array<Float>
для представления комплексных векторов, вы должны определить тип ComplexVector
.Это может позволить вам применять инварианты (например, всегда есть столько реальных значений, сколько мнимых значений) и добавлять удобные операции (например, func append(real: Float, imaginary: Float)
, который работает с обоими одновременно, гарантируя, что вы никогда не забудете добавить один из массивов).
В заключение,
Здесь много чего происходит, поэтому я не могу предвосхитить каждый вопрос и объяснить его заранее.Я призываю вас потратить некоторое время, прочитать это, и не стесняйтесь задавать мне любые дополнительные вопросы.
Я подозреваю, что допустил ошибки во время моегорефакторинг (потому что у меня не было тестовых примеров для работы), но код достаточно модульный, чтобы его было очень просто изолировать и исправить, а также ошибки.