├── Tests ├── LinuxMain.swift └── TunaTests │ ├── XCTestManifests.swift │ ├── TunaTests.swift │ └── PitchyTests.swift ├── Sources └── Tuna │ ├── Estimation │ ├── EstimationError.swift │ ├── Strategies │ │ ├── MaxValueEstimator.swift │ │ ├── QuadradicEstimator.swift │ │ ├── BarycentricEstimator.swift │ │ ├── JainsEstimator.swift │ │ ├── YINEstimator.swift │ │ ├── QuinnsFirstEstimator.swift │ │ ├── QuinnsSecondEstimator.swift │ │ └── HPSEstimator.swift │ ├── LocationEstimator.swift │ ├── EstimationStrategy.swift │ └── Estimator.swift │ ├── Pitch │ ├── Error.swift │ ├── FrequencyValidator.swift │ ├── Calculators │ │ ├── PitchCalculator.swift │ │ ├── WaveCalculator.swift │ │ └── NoteCalculator.swift │ └── Data │ │ ├── Pitch.swift │ │ ├── AcousticWave.swift │ │ └── Note.swift │ ├── Transform │ ├── Transformer.swift │ └── Strategies │ │ ├── PassthroughTransformer.swift │ │ ├── YINTransformer.swift │ │ └── FFTTransformer.swift │ ├── Utilities │ ├── Array+Extensions.swift │ ├── Buffer.swift │ └── YINUtil.swift │ ├── SignalTracking │ ├── SignalTracker.swift │ └── Units │ │ ├── OutputSignalTracker.swift │ │ ├── SimulatorSignalTracker.swift │ │ └── InputSignalTracker.swift │ └── PitchEngine.swift ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── Tuna.xcscheme ├── Package.swift ├── LICENSE ├── .gitignore └── README.md /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import TunaTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += TunaTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/EstimationError.swift: -------------------------------------------------------------------------------- 1 | public enum EstimationError: Error { 2 | case emptyBuffer 3 | case unknownMaxIndex 4 | case unknownLocation 5 | case unknownFrequency 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/Error.swift: -------------------------------------------------------------------------------- 1 | public enum PitchError: Error { 2 | case invalidFrequency 3 | case invalidWavelength 4 | case invalidPeriod 5 | case invalidPitchIndex 6 | case invalidOctave 7 | } 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/TunaTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(TunaTests.allTests), 7 | testCase(PitchyTests.allTests) 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/MaxValueEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct MaxValueEstimator: LocationEstimator { 4 | 5 | func estimateLocation(buffer: Buffer) throws -> Int { 6 | try maxBufferIndex(from: buffer.elements) 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Tuna/Transform/Transformer.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | /// A protocol for Buffer transoformers 4 | protocol Transformer { 5 | /// Transform an AVAudioPCMBuffer to a Buffer 6 | /// - Parameter buffer: The AVAudioPCMBuffer input 7 | func transform(buffer: AVAudioPCMBuffer) throws -> Buffer 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Tuna/Utilities/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | extension Array where Element : Comparable { 2 | static func fromUnsafePointer(_ data: UnsafePointer, count: Int) -> [Element] { 3 | let buffer = UnsafeBufferPointer(start: data, count: count) 4 | return Array(buffer) 5 | } 6 | 7 | var maxIndex: Int? { 8 | enumerated().max(by: { $1.element > $0.element })?.offset 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Tuna/Utilities/Buffer.swift: -------------------------------------------------------------------------------- 1 | struct Buffer { 2 | let elements: [Float] 3 | let realElements: [Float]? 4 | let imagElements: [Float]? 5 | 6 | var count: Int { 7 | elements.count 8 | } 9 | 10 | // MARK: - Initialization 11 | 12 | init(elements: [Float], realElements: [Float]? = nil, imagElements: [Float]? = nil) { 13 | self.elements = elements 14 | self.realElements = realElements 15 | self.imagElements = imagElements 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/LocationEstimator.swift: -------------------------------------------------------------------------------- 1 | protocol LocationEstimator: Estimator { 2 | func estimateLocation(buffer: Buffer) throws -> Int 3 | } 4 | 5 | // MARK: - Default implementation 6 | 7 | extension LocationEstimator { 8 | 9 | var transformer: Transformer { FFTTransformer() } 10 | 11 | func estimateFrequency(sampleRate: Float, buffer: Buffer) throws -> Float { 12 | let location = try estimateLocation(buffer: buffer) 13 | return estimateFrequency(sampleRate: sampleRate, location: location, bufferCount: buffer.count) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/FrequencyValidator.swift: -------------------------------------------------------------------------------- 1 | public struct FrequencyValidator { 2 | public static var range = 20.0 ... 4190.0 3 | 4 | public static let minimumFrequency = range.lowerBound 5 | public static let maximumFrequency = range.upperBound 6 | 7 | public static func isValid(frequency: Double) -> Bool { 8 | frequency > 0.0 && range.contains(frequency) 9 | } 10 | 11 | public static func validate(frequency: Double) throws { 12 | if !isValid(frequency: frequency) { 13 | throw PitchError.invalidFrequency 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Tuna/Transform/Strategies/PassthroughTransformer.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | struct PassthroughTransformer: Transformer { 4 | 5 | enum PassthroughTransformerError: Error { 6 | case floatChannelDataIsNil 7 | } 8 | 9 | func transform(buffer: AVAudioPCMBuffer) throws -> Buffer { 10 | guard let pointer = buffer.floatChannelData else { 11 | throw PassthroughTransformerError.floatChannelDataIsNil 12 | } 13 | 14 | let elements = Array.fromUnsafePointer(pointer.pointee, count: Int(buffer.frameLength)) 15 | return Buffer(elements: elements) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Tuna/Transform/Strategies/YINTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YINTransformer.swift 3 | // Beethoven 4 | // 5 | // Created by Guillaume Laurent on 10/10/16. 6 | // Adapted from https://code.soundsoftware.ac.uk/projects/pyin/repository 7 | // by Matthias Mauch, Centre for Digital Music, Queen Mary, University of London. 8 | // 9 | 10 | import Foundation 11 | import AVFoundation 12 | 13 | struct YINTransformer: Transformer { 14 | 15 | func transform(buffer: AVAudioPCMBuffer) throws -> Buffer { 16 | let buffer = try PassthroughTransformer().transform(buffer: buffer) 17 | let diffElements = YINUtil.differenceA(buffer: buffer.elements) 18 | return Buffer(elements: diffElements) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/QuadradicEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct QuadradicEstimator: LocationEstimator { 4 | 5 | func estimateLocation(buffer: Buffer) throws -> Int { 6 | let elements = buffer.elements 7 | let maxIndex = try maxBufferIndex(from: elements) 8 | 9 | let y2 = abs(elements[maxIndex]) 10 | let y1 = maxIndex == 0 ? y2 : abs(elements[maxIndex - 1]) 11 | let y3 = maxIndex == elements.count - 1 ? y2 : abs(elements[maxIndex + 1]) 12 | let d = (y3 - y1) / (2 * (2 * y2 - y1 - y3)) 13 | 14 | let location = maxIndex + Int(round(d)) 15 | 16 | return sanitize(location: location, reserveLocation: maxIndex, elements: elements) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Tuna/SignalTracking/SignalTracker.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | public protocol SignalTrackerDelegate: class { 4 | func signalTracker(_ signalTracker: SignalTracker, didReceiveBuffer buffer: AVAudioPCMBuffer, atTime time: AVAudioTime) 5 | func signalTrackerWentBelowLevelThreshold(_ signalTracker: SignalTracker) 6 | } 7 | 8 | public enum SignalTrackerMode { 9 | case record, playback 10 | } 11 | 12 | public protocol SignalTracker: class { 13 | var mode: SignalTrackerMode { get } 14 | var levelThreshold: Float? { get set } 15 | var peakLevel: Float? { get } 16 | var averageLevel: Float? { get } 17 | var delegate: SignalTrackerDelegate? { get set } 18 | 19 | func start() throws 20 | func stop() 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/BarycentricEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct BarycentricEstimator: LocationEstimator { 4 | 5 | func estimateLocation(buffer: Buffer) throws -> Int { 6 | let elements = buffer.elements 7 | let maxIndex = try maxBufferIndex(from: elements) 8 | 9 | let y2 = abs(elements[maxIndex]) 10 | let y1 = maxIndex == 0 ? y2 : abs(elements[maxIndex - 1]) 11 | let y3 = maxIndex == elements.count - 1 ? y2 : abs(elements[maxIndex + 1]) 12 | let d = (y3 - y1) / (y1 + y2 + y3) 13 | 14 | guard !d.isNaN else { 15 | throw EstimationError.unknownLocation 16 | } 17 | 18 | let location = maxIndex + Int(round(d)) 19 | 20 | return sanitize(location: location, reserveLocation: maxIndex, elements: elements) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/EstimationStrategy.swift: -------------------------------------------------------------------------------- 1 | public enum EstimationStrategy: CaseIterable { 2 | case maxValue 3 | case quadradic 4 | case barycentric 5 | case quinnsFirst 6 | case quinnsSecond 7 | case jains 8 | case hps 9 | case yin 10 | } 11 | 12 | extension EstimationStrategy { 13 | var estimator: Estimator { 14 | switch self { 15 | case .maxValue: return MaxValueEstimator() 16 | case .quadradic: return QuadradicEstimator() 17 | case .barycentric: return BarycentricEstimator() 18 | case .quinnsFirst: return QuinnsFirstEstimator() 19 | case .quinnsSecond: return QuinnsSecondEstimator() 20 | case .jains: return JainsEstimator() 21 | case .hps: return HPSEstimator() 22 | case .yin: return YINEstimator() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/JainsEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct JainsEstimator: LocationEstimator { 4 | 5 | func estimateLocation(buffer: Buffer) throws -> Int { 6 | let elements = buffer.elements 7 | let maxIndex = try maxBufferIndex(from: elements) 8 | 9 | let y2 = abs(elements[maxIndex]) 10 | let y1 = maxIndex == 0 ? y2 : abs(elements[maxIndex - 1]) 11 | let y3 = maxIndex == elements.count - 1 ? y2 : abs(elements[maxIndex + 1]) 12 | let location: Int 13 | 14 | if y1 > y3 { 15 | let a = y2 / y1 16 | let d = a / (1 + a) 17 | location = maxIndex - 1 + Int(round(d)) 18 | } else { 19 | let a = y3 / y2 20 | let d = a / (1 + a) 21 | location = maxIndex + Int(round(d)) 22 | } 23 | 24 | return sanitize(location: location, reserveLocation: maxIndex, elements: elements) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/YINEstimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YINEstimator.swift 3 | // Beethoven 4 | // 5 | // Created by Guillaume Laurent on 18/10/16. 6 | // Copyright © 2016 Vadym Markov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct YINEstimator: Estimator { 12 | 13 | let transformer: Transformer = YINTransformer() 14 | let threshold: Float = 0.05 15 | 16 | func estimateFrequency(sampleRate: Float, buffer: Buffer) throws -> Float { 17 | var elements = buffer.elements 18 | 19 | YINUtil.cumulativeDifference(yinBuffer: &elements) 20 | 21 | let tau = YINUtil.absoluteThreshold(yinBuffer: elements, withThreshold: threshold) 22 | let f0: Float 23 | 24 | if tau != 0 { 25 | let interpolatedTau = YINUtil.parabolicInterpolation(yinBuffer: elements, tau: tau) 26 | f0 = sampleRate / interpolatedTau 27 | } else { 28 | f0 = 0.0 29 | } 30 | 31 | return f0 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Estimator.swift: -------------------------------------------------------------------------------- 1 | protocol Estimator { 2 | var transformer: Transformer { get } 3 | func estimateFrequency(sampleRate: Float, buffer: Buffer) throws -> Float 4 | func estimateFrequency(sampleRate: Float, location: Int, bufferCount: Int) -> Float 5 | } 6 | 7 | // MARK: - Default implementations 8 | 9 | extension Estimator { 10 | func estimateFrequency(sampleRate: Float, location: Int, bufferCount: Int) -> Float { 11 | Float(location) * sampleRate / (Float(bufferCount) * 2) 12 | } 13 | 14 | func maxBufferIndex(from buffer: [Float]) throws -> Int { 15 | guard !buffer.isEmpty else { 16 | throw EstimationError.emptyBuffer 17 | } 18 | 19 | guard let index = buffer.maxIndex else { 20 | throw EstimationError.unknownMaxIndex 21 | } 22 | 23 | return index 24 | } 25 | 26 | func sanitize(location: Int, reserveLocation: Int, elements: [Float]) -> Int { 27 | (location >= 0 && location < elements.count) ? location : reserveLocation 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Tuna", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "Tuna", 12 | targets: ["Tuna"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "Tuna", 23 | dependencies: []), 24 | .testTarget( 25 | name: "TunaTests", 26 | dependencies: ["Tuna"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vasilis Akoinoglou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/QuinnsFirstEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct QuinnsFirstEstimator: LocationEstimator { 4 | 5 | func estimateLocation(buffer: Buffer) throws -> Int { 6 | let elements = buffer.elements 7 | let maxIndex = try maxBufferIndex(from: elements) 8 | 9 | guard let realElements = buffer.realElements, let imagElements = buffer.imagElements else { 10 | return maxIndex 11 | } 12 | 13 | let realp = realElements 14 | let imagp = imagElements 15 | 16 | let prevIndex = maxIndex == 0 ? maxIndex : maxIndex - 1 17 | let nextIndex = maxIndex == buffer.count - 1 ? maxIndex : maxIndex + 1 18 | let divider = pow(realp[maxIndex], 2.0) + pow(imagp[maxIndex], 2.0) 19 | 20 | let ap = (realp[nextIndex] * realp[maxIndex] + imagp[nextIndex] * imagp[maxIndex]) / divider 21 | let dp = -ap / (1.0 - ap) 22 | let am = (realp[prevIndex] * realp[maxIndex] + imagp[prevIndex] * imagp[maxIndex]) / divider 23 | let dm = am / (1.0 - am) 24 | let d = dp > 0 && dm > 0 ? dp : dm 25 | 26 | let location = maxIndex + Int(round(d)) 27 | 28 | return sanitize(location: location, reserveLocation: maxIndex, elements: elements) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/QuinnsSecondEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct QuinnsSecondEstimator: LocationEstimator { 4 | 5 | func estimateLocation(buffer: Buffer) throws -> Int { 6 | let elements = buffer.elements 7 | let maxIndex = try maxBufferIndex(from: elements) 8 | 9 | guard let realElements = buffer.realElements, let imagElements = buffer.imagElements else { 10 | return maxIndex 11 | } 12 | 13 | let realp = realElements 14 | let imagp = imagElements 15 | 16 | let prevIndex = maxIndex == 0 ? maxIndex : maxIndex - 1 17 | let nextIndex = maxIndex == buffer.count - 1 ? maxIndex : maxIndex + 1 18 | let divider = pow(realp[maxIndex], 2.0) + pow(imagp[maxIndex], 2.0) 19 | 20 | let ap = (realp[nextIndex] * realp[maxIndex] + imagp[nextIndex] * imagp[maxIndex]) / divider 21 | let dp = -ap / (1.0 - ap) 22 | let am = (realp[prevIndex] * realp[maxIndex] + imagp[prevIndex] * imagp[maxIndex]) / divider 23 | let dm = am / (1.0 - am) 24 | let d = (dp + dm) / 2 + tau(dp * dp) - tau(dm * dm) 25 | 26 | let location = maxIndex + Int(round(d)) 27 | 28 | return sanitize(location: location, reserveLocation: maxIndex, elements: elements) 29 | } 30 | 31 | private func tau(_ x: Float) -> Float { 32 | let p1 = log(3 * pow(x, 2.0) + 6 * x + 1) 33 | let part1 = x + 1 - sqrt(2/3) 34 | let part2 = x + 1 + sqrt(2/3) 35 | let p2 = log(part1 / part2) 36 | 37 | return 1/4 * p1 - sqrt(6)/24 * p2 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/Calculators/PitchCalculator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PitchCalculator { 4 | 5 | public static func offsets(forFrequency frequency: Double) throws -> Pitch.Offsets { 6 | let note = try Note(frequency: frequency) 7 | let higherNote = try note.higher() 8 | let lowerNote = try note.lower() 9 | 10 | let closestNote = abs(higherNote.frequency - frequency) 11 | < abs(lowerNote.frequency - frequency) 12 | ? higherNote 13 | : lowerNote 14 | 15 | let firstOffset = Pitch.Offset( 16 | note: note, 17 | frequency: frequency - note.frequency, 18 | percentage: (frequency - note.frequency) * 100 / abs(note.frequency - closestNote.frequency), 19 | cents: try cents(frequency1: note.frequency, frequency2: frequency) 20 | ) 21 | 22 | let secondOffset = Pitch.Offset( 23 | note: closestNote, 24 | frequency: frequency - closestNote.frequency, 25 | percentage: (frequency - closestNote.frequency) * 100 / abs(note.frequency - closestNote.frequency), 26 | cents: try cents(frequency1: closestNote.frequency, frequency2: frequency) 27 | ) 28 | 29 | return Pitch.Offsets(firstOffset, secondOffset) 30 | } 31 | 32 | public static func cents(frequency1: Double, frequency2: Double) throws -> Double { 33 | try FrequencyValidator.validate(frequency: frequency1) 34 | try FrequencyValidator.validate(frequency: frequency2) 35 | return 1200.0 * log2(frequency2 / frequency1) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Tuna/Estimation/Strategies/HPSEstimator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct HPSEstimator: LocationEstimator { 4 | 5 | private let harmonics = 5 6 | private let minIndex = 20 7 | 8 | func estimateLocation(buffer: Buffer) throws -> Int { 9 | var spectrum = buffer.elements 10 | let maxIndex = spectrum.count - 1 11 | var maxHIndex = spectrum.count / harmonics 12 | 13 | if maxIndex < maxHIndex { 14 | maxHIndex = maxIndex 15 | } 16 | 17 | var location = minIndex 18 | 19 | for j in minIndex...maxHIndex { 20 | for i in 1...harmonics { 21 | spectrum[j] *= spectrum[j * i] 22 | } 23 | 24 | if spectrum[j] > spectrum[location] { 25 | location = j 26 | } 27 | } 28 | 29 | var max2 = minIndex 30 | var maxsearch = location * 3 / 4 31 | 32 | if location > (minIndex + 1) { 33 | if maxsearch <= (minIndex + 1) { 34 | maxsearch = location 35 | } 36 | 37 | // swiftlint:disable for_where 38 | for i in (minIndex + 1).. spectrum[max2] { 40 | max2 = i 41 | } 42 | } 43 | 44 | if abs(max2 * 2 - location) < 4 { 45 | if spectrum[max2] / spectrum[location] > 0.2 { 46 | location = max2 47 | } 48 | } 49 | } 50 | 51 | return sanitize(location: location, reserveLocation: maxIndex, elements: spectrum) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Tuna/SignalTracking/Units/OutputSignalTracker.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | final class OutputSignalTracker: SignalTracker { 4 | weak var delegate: SignalTrackerDelegate? 5 | var levelThreshold: Float? 6 | 7 | private let bufferSize: AVAudioFrameCount 8 | private let audioUrl: URL 9 | private var audioEngine: AVAudioEngine! 10 | private var audioPlayer: AVAudioPlayerNode! 11 | private let bus = 0 12 | 13 | var peakLevel: Float? { 14 | return 0.0 15 | } 16 | 17 | var averageLevel: Float? { 18 | return 0.0 19 | } 20 | 21 | var mode: SignalTrackerMode { 22 | .playback 23 | } 24 | 25 | // MARK: - Initialization 26 | 27 | required init(audioUrl: URL, bufferSize: AVAudioFrameCount = 2048, delegate: SignalTrackerDelegate? = nil) { 28 | self.audioUrl = audioUrl 29 | self.bufferSize = bufferSize 30 | self.delegate = delegate 31 | } 32 | 33 | // MARK: - Tracking 34 | 35 | func start() throws { 36 | 37 | #if os(iOS) 38 | let session = AVAudioSession.sharedInstance() 39 | try session.setCategory(.playback) 40 | #endif 41 | 42 | audioEngine = AVAudioEngine() 43 | audioPlayer = AVAudioPlayerNode() 44 | 45 | let audioFile = try AVAudioFile(forReading: audioUrl) 46 | 47 | audioEngine.attach(audioPlayer) 48 | audioEngine.connect(audioPlayer, to: audioEngine.outputNode, format: audioFile.processingFormat) 49 | audioPlayer.scheduleFile(audioFile, at: nil, completionHandler: nil) 50 | 51 | audioEngine.outputNode.installTap(onBus: bus, bufferSize: bufferSize, format: nil) { buffer, time in 52 | DispatchQueue.main.async { 53 | self.delegate?.signalTracker(self, didReceiveBuffer: buffer, atTime: time) 54 | } 55 | } 56 | 57 | audioEngine.prepare() 58 | try audioEngine.start() 59 | 60 | audioPlayer.play() 61 | } 62 | 63 | func stop() { 64 | audioPlayer.stop() 65 | audioEngine.stop() 66 | audioEngine.reset() 67 | audioEngine = nil 68 | audioPlayer = nil 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/Data/Pitch.swift: -------------------------------------------------------------------------------- 1 | /// A structure representing a Pitch 2 | public struct Pitch { 3 | 4 | /// A tuple holding offset information 5 | public typealias Offset = (note: Note, frequency: Double, percentage: Double, cents: Double) 6 | 7 | /// A structure encapsulating a pair of offsets 8 | public struct Offsets { 9 | /// The lower offset 10 | public let lower: Pitch.Offset 11 | 12 | /// The higher offset 13 | public let higher: Pitch.Offset 14 | 15 | /// The closest offset 16 | public var closest: Pitch.Offset { 17 | abs(lower.frequency) < abs(higher.frequency) ? lower : higher 18 | } 19 | 20 | // MARK: - Initialization 21 | 22 | /// Initialize a pair of Offsets 23 | /// - Parameters: 24 | /// - first: The first offset 25 | /// - second: The second offset 26 | public init(_ first: Offset, _ second: Offset) { 27 | let lowerFirst = first.note.frequency < second.note.frequency 28 | self.lower = lowerFirst ? first : second 29 | self.higher = lowerFirst ? second : first 30 | } 31 | } 32 | 33 | /// The frequency of the pitch 34 | public let frequency: Double 35 | 36 | /// The wave of the pitch 37 | public let wave: AcousticWave 38 | 39 | /// The offsets of the pitch 40 | public let offsets: Offsets 41 | 42 | /// The closest note to the pitch 43 | public var note: Note { 44 | return offsets.closest.note 45 | } 46 | 47 | /// The closest offset to the pitch 48 | public var closestOffset: Offset { 49 | return offsets.closest 50 | } 51 | 52 | // MARK: - Initialization 53 | 54 | /// Initialize a Pitch from a frequency 55 | /// - Parameter frequency: The frequency of the Pitch 56 | /// - Throws: An error if the acoustic wave or the offsets cannot be calculated 57 | public init(frequency: Double) throws { 58 | try FrequencyValidator.validate(frequency: frequency) 59 | self.frequency = frequency 60 | self.wave = try AcousticWave(frequency: frequency) 61 | self.offsets = try PitchCalculator.offsets(forFrequency: frequency) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/Calculators/WaveCalculator.swift: -------------------------------------------------------------------------------- 1 | public struct WaveCalculator { 2 | 3 | public static var wavelengthBounds: ClosedRange { 4 | let minimum = try! wavelength(forFrequency: FrequencyValidator.maximumFrequency) 5 | let maximum = try! wavelength(forFrequency: FrequencyValidator.minimumFrequency) 6 | 7 | return minimum ... maximum 8 | } 9 | 10 | public static var periodBounds: ClosedRange { 11 | let bounds = wavelengthBounds 12 | let minimum = try! period(forWavelength: bounds.lowerBound) 13 | let maximum = try! period(forWavelength: bounds.upperBound) 14 | 15 | return minimum ... maximum 16 | } 17 | 18 | // MARK: - Validators 19 | 20 | public static func isValid(wavelength: Double) -> Bool { 21 | wavelength > 0.0 && wavelengthBounds.contains(wavelength) 22 | } 23 | 24 | public static func validate(wavelength: Double) throws { 25 | if !isValid(wavelength: wavelength) { 26 | throw PitchError.invalidWavelength 27 | } 28 | } 29 | 30 | public static func isValid(period: Double) -> Bool { 31 | period > 0.0 && periodBounds.contains(period) 32 | } 33 | 34 | public static func validate(period: Double) throws { 35 | if !isValid(period: period) { 36 | throw PitchError.invalidPeriod 37 | } 38 | } 39 | 40 | // MARK: - Conversions 41 | 42 | public static func frequency(forWavelength wavelength: Double) throws -> Double { 43 | try WaveCalculator.validate(wavelength: wavelength) 44 | return AcousticWave.speed / wavelength 45 | } 46 | 47 | public static func wavelength(forFrequency frequency: Double) throws -> Double { 48 | try FrequencyValidator.validate(frequency: frequency) 49 | return AcousticWave.speed / frequency 50 | } 51 | 52 | public static func wavelength(forPeriod period: Double) throws -> Double { 53 | try WaveCalculator.validate(period: period) 54 | return period * AcousticWave.speed 55 | } 56 | 57 | public static func period(forWavelength wavelength: Double) throws -> Double { 58 | try WaveCalculator.validate(wavelength: wavelength) 59 | return wavelength / AcousticWave.speed 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/Data/AcousticWave.swift: -------------------------------------------------------------------------------- 1 | public struct AcousticWave { 2 | 3 | /// The speed of sound in air (m/s) 4 | public static let speed: Double = 343 5 | 6 | /// The frequency of the wave 7 | public let frequency: Double 8 | 9 | /// The wavelength 10 | public let wavelength: Double 11 | 12 | /// The period of the wave 13 | public let period: Double 14 | 15 | /// Up to 16 harmonic pitches 16 | public var harmonics: [Pitch] { 17 | var pitches = [Pitch]() 18 | 19 | do { 20 | for index in 1...16 { 21 | try pitches.append(Pitch(frequency: Double(index) * frequency)) 22 | } 23 | } catch { 24 | debugPrint(error) 25 | } 26 | 27 | return pitches 28 | } 29 | 30 | // MARK: - Initialization 31 | 32 | /// Initialize a wave with a frequency 33 | /// - Parameter frequency: The frequency of the wave 34 | /// - Throws: An error in case wavelength or period cannot be calculated 35 | public init(frequency: Double) throws { 36 | try FrequencyValidator.validate(frequency: frequency) 37 | self.frequency = frequency 38 | wavelength = try WaveCalculator.wavelength(forFrequency: frequency) 39 | period = try WaveCalculator.period(forWavelength: wavelength) 40 | } 41 | 42 | /// Initialize a wave with a wavelength 43 | /// - Parameter wavelength: The wavelength 44 | /// - Throws: An error in case frequency or period cannot be calculated 45 | public init(wavelength: Double) throws { 46 | try WaveCalculator.validate(wavelength: wavelength) 47 | self.wavelength = wavelength 48 | frequency = try WaveCalculator.frequency(forWavelength: wavelength) 49 | period = try WaveCalculator.period(forWavelength: wavelength) 50 | } 51 | 52 | /// Initialize a wave with a period 53 | /// - Parameter period: The period of the wave 54 | /// - Throws: An error in case wavelength or frequency cannot be calculated 55 | public init(period: Double) throws { 56 | try WaveCalculator.validate(period: period) 57 | self.period = period 58 | wavelength = try WaveCalculator.wavelength(forPeriod: period) 59 | frequency = try WaveCalculator.frequency(forWavelength: wavelength) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | */.DS_Store 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | Sources/Tuna/.DS_Store 94 | .DS_Store 95 | -------------------------------------------------------------------------------- /Tests/TunaTests/TunaTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Accelerate 3 | import AVFoundation 4 | @testable import Tuna 5 | 6 | final class TunaTests: XCTestCase { 7 | 8 | var estimator: Estimator! 9 | 10 | override func setUp() { 11 | super.setUp() 12 | estimator = HPSEstimator() 13 | } 14 | 15 | func testEstimationFactory() { 16 | 17 | // QuadradicEstimator 18 | XCTAssertTrue(EstimationStrategy.quadradic.estimator is QuadradicEstimator) 19 | 20 | // Barycentric 21 | XCTAssertTrue(EstimationStrategy.barycentric.estimator is BarycentricEstimator) 22 | 23 | // QuinnsFirst 24 | XCTAssertTrue(EstimationStrategy.quinnsFirst.estimator is QuinnsFirstEstimator) 25 | 26 | 27 | // QuinnsSecond 28 | XCTAssertTrue(EstimationStrategy.quinnsSecond.estimator is QuinnsSecondEstimator) 29 | 30 | 31 | // Jains 32 | XCTAssertTrue(EstimationStrategy.jains.estimator is JainsEstimator) 33 | 34 | 35 | // HPS 36 | XCTAssertTrue(EstimationStrategy.hps.estimator is HPSEstimator) 37 | 38 | 39 | // YIN 40 | XCTAssertTrue(EstimationStrategy.yin.estimator is YINEstimator) 41 | 42 | 43 | // MaxValue 44 | XCTAssertTrue(EstimationStrategy.maxValue.estimator is MaxValueEstimator) 45 | } 46 | 47 | func testEstimatorBuffer() { 48 | let array: [Float] = [0.1, 0.3, 0.2] 49 | let result = try! estimator.maxBufferIndex(from: array) 50 | 51 | XCTAssertEqual(result, 1, "returns the index of the max element in the array") 52 | } 53 | 54 | func testEstimatorSanitizeInBounds() { 55 | let array: [Float] = [0.1, 0.3, 0.2] 56 | let result = estimator.sanitize(location: 1, reserveLocation: 0, elements: array) 57 | 58 | XCTAssertEqual(result, 1, "returns the passed location if it doesn't extend array bounds") 59 | } 60 | 61 | func testEstimatorOutOfBounds() { 62 | let array: [Float] = [0.1, 0.3, 0.2] 63 | let result = estimator.sanitize(location: 4, reserveLocation: 0, elements: array) 64 | 65 | XCTAssertEqual(result, 0, "returns the reserve location if the passed location extends array bounds") 66 | } 67 | 68 | func testArrayExtensions() { 69 | var array = [0.1, 0.3, 0.2] 70 | let result = Array.fromUnsafePointer(&array, count: 3) 71 | 72 | XCTAssertEqual(result, array) 73 | XCTAssertEqual(array.maxIndex, 1) 74 | } 75 | 76 | func testBuffer() { 77 | let buffer = Buffer(elements: [0.1, 0.2, 0.3]) 78 | XCTAssertEqual(buffer.count, 3) 79 | } 80 | 81 | func testFFT() { 82 | let transformer = FFTTransformer() 83 | let array: [Float] = [0.1, 0.2, 0.3] 84 | var expected = [Float](repeating: 0.0, count: array.count) 85 | vvsqrtf(&expected, array, [Int32(array.count)]) 86 | 87 | XCTAssertEqual(transformer.sqrtq(array), expected, "returns the array's square") 88 | } 89 | 90 | static var allTests = [ 91 | ("testEstimationFactory", testEstimationFactory), 92 | ("testEstimatorBuffer", testEstimatorBuffer), 93 | ("testEstimatorSanitizeInBounds", testEstimatorSanitizeInBounds), 94 | ("testEstimatorOutOfBounds", testEstimatorOutOfBounds), 95 | ("testArrayExtensions", testArrayExtensions), 96 | ("testBuffer", testBuffer), 97 | ("testFFT", testFFT), 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Tuna/Pitch/Data/Note.swift: -------------------------------------------------------------------------------- 1 | public struct Note: CustomStringConvertible { 2 | 3 | /// The letter of a music note in English Notation 4 | public enum Letter: String, CaseIterable, CustomStringConvertible { 5 | case C = "C" 6 | case CSharp = "C#" 7 | case D = "D" 8 | case DSharp = "D#" 9 | case E = "E" 10 | case F = "F" 11 | case FSharp = "F#" 12 | case G = "G" 13 | case GSharp = "G#" 14 | case A = "A" 15 | case ASharp = "A#" 16 | case B = "B" 17 | 18 | public var description: String { rawValue } 19 | } 20 | 21 | /// The index of the note 22 | public let index: Int 23 | 24 | /// The letter of the note in English Notation 25 | public let letter: Letter 26 | 27 | /// The octave of the note 28 | public let octave: Int 29 | 30 | /// The frequency of the note 31 | public let frequency: Double 32 | 33 | /// The corresponding wave of the note 34 | public let wave: AcousticWave 35 | 36 | /// A string description of the note including octave (eg A4) 37 | public var description: String { 38 | "\(self.letter)\(self.octave)" 39 | } 40 | 41 | // MARK: - Initialization 42 | 43 | /// Initialize a Note from an index 44 | /// - Parameter index: The index of the note 45 | /// - Throws: An error if the rest of the components cannot be calculated 46 | public init(index: Int) throws { 47 | self.index = index 48 | letter = try NoteCalculator.letter(forIndex: index) 49 | octave = try NoteCalculator.octave(forIndex: index) 50 | frequency = try NoteCalculator.frequency(forIndex: index) 51 | wave = try AcousticWave(frequency: frequency) 52 | } 53 | 54 | /// Initialize a Note from a frequency 55 | /// - Parameter frequency: The frequency of the note 56 | /// - Throws: An error if the rest of the components cannot be calculated 57 | public init(frequency: Double) throws { 58 | index = try NoteCalculator.index(forFrequency: frequency) 59 | letter = try NoteCalculator.letter(forIndex: index) 60 | octave = try NoteCalculator.octave(forIndex: index) 61 | self.frequency = try NoteCalculator.frequency(forIndex: index) 62 | wave = try AcousticWave(frequency: frequency) 63 | } 64 | 65 | /// Initialize a Note from a Letter & Octave 66 | /// - Parameters: 67 | /// - letter: The letter of the note 68 | /// - octave: The octave of the note 69 | /// - Throws: An error if the rest of the components cannot be calculated 70 | public init(letter: Letter, octave: Int) throws { 71 | self.letter = letter 72 | self.octave = octave 73 | index = try NoteCalculator.index(forLetter: letter, octave: octave) 74 | frequency = try NoteCalculator.frequency(forIndex: index) 75 | wave = try AcousticWave(frequency: frequency) 76 | } 77 | 78 | // MARK: - Neighbor Notes 79 | 80 | /// One semitone lower 81 | /// - Throws: An error if the semitone is out of bounds 82 | /// - Returns: A note that is one semitone lower 83 | public func lower() throws -> Note { 84 | try Note(index: index - 1) 85 | } 86 | 87 | /// One semitone higher 88 | /// - Throws: An error if the semitone is out of bounds 89 | /// - Returns: A note that is one semitone higher 90 | public func higher() throws -> Note { 91 | try Note(index: index + 1) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Tuna.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Sources/Tuna/SignalTracking/Units/SimulatorSignalTracker.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | /* 4 | * A mock implememtation of SignalTracker useful for unit testing and/or running in the simulator. 5 | * 6 | * It creates a series of PCM buffers filled with sine waves of given frequencies, 7 | * and passes the buffers to the delegate every delayMs milliseconds. 8 | * 9 | * Example: 10 | * 11 | * #if (arch(i386) || arch(x86_64)) && os(iOS) 12 | * // Simulator 13 | * let frequencies = try? [ 14 | * 391.995435981749, 15 | * 391.995435981749, 16 | * 415.304697579945, 17 | * Note(letter: Note.Letter.A, octave: 4).frequency, 18 | * 466.163761518090, 19 | * 466.163761518090, 20 | * Note(letter: Note.Letter.A, octave: 4).frequency, 21 | * 415.304697579945, 22 | * 391.995435981749 23 | * ] 24 | * let signalTracker = SimulatorSignalTracker(frequencies: frequencies, delayMs: 1000) 25 | * let pitchEngine = PitchEngine(config: config, signalTracker: signalTracker, delegate: delegate) 26 | * #else 27 | * // Device 28 | * let pitchEngine = PitchEngine(config: config, delegate: delegate) 29 | * #endif 30 | * 31 | */ 32 | public final class SimulatorSignalTracker: SignalTracker { 33 | 34 | private static let sampleRate = 8000.0 35 | private static let sampleCount = 1024 36 | 37 | public var mode: SignalTrackerMode = .record 38 | public var levelThreshold: Float? 39 | public var peakLevel: Float? 40 | public var averageLevel: Float? 41 | public weak var delegate: SignalTrackerDelegate? 42 | 43 | private let frequencies: [Double]? 44 | private let delay: Int 45 | 46 | public init(delegate: SignalTrackerDelegate? = nil, frequencies: [Double]? = nil, delayMs: Int = 0) { 47 | self.delegate = delegate 48 | self.frequencies = frequencies 49 | self.delay = delayMs 50 | } 51 | 52 | public func start() throws { 53 | guard let frequencies = self.frequencies else { return } 54 | 55 | let time = AVAudioTime(sampleTime: 0, atRate: SimulatorSignalTracker.sampleRate) 56 | var i = 0 57 | 58 | for frequency in frequencies { 59 | let buffer = createPCMBuffer(frequency) 60 | 61 | if i == 0 { 62 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { 63 | self.delegate?.signalTracker(self, didReceiveBuffer: buffer, atTime: time) 64 | } 65 | } else { 66 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay * i)) { 67 | self.delegate?.signalTracker(self, didReceiveBuffer: buffer, atTime: time) 68 | } 69 | } 70 | 71 | i += 1 72 | } 73 | 74 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay * i)) { 75 | self.delegate?.signalTrackerWentBelowLevelThreshold(self) 76 | } 77 | } 78 | 79 | public func stop() {} 80 | 81 | private func createPCMBuffer(_ frequency: Double) -> AVAudioPCMBuffer { 82 | let format = AVAudioFormat(standardFormatWithSampleRate: SimulatorSignalTracker.sampleRate, channels: 1) 83 | let buffer = AVAudioPCMBuffer(pcmFormat: format!, frameCapacity: AVAudioFrameCount(SimulatorSignalTracker.sampleCount)) 84 | 85 | guard let channelData = buffer?.floatChannelData else { return buffer! } 86 | 87 | let velocity = Float32(2.0 * .pi * frequency / SimulatorSignalTracker.sampleRate) 88 | 89 | for i in 0.. { 28 | let minimum = try! index(forFrequency: FrequencyValidator.minimumFrequency) 29 | let maximum = try! index(forFrequency: FrequencyValidator.maximumFrequency) 30 | 31 | return minimum ... maximum 32 | } 33 | 34 | public static var octaveBounds: ClosedRange { 35 | let bounds = indexBounds 36 | let minimum = try! octave(forIndex: bounds.lowerBound) 37 | let maximum = try! octave(forIndex: bounds.upperBound) 38 | 39 | return minimum ... maximum 40 | } 41 | 42 | // MARK: - Validators 43 | 44 | public static func isValid(index: Int) -> Bool { 45 | indexBounds.contains(index) 46 | } 47 | 48 | public static func validate(index: Int) throws { 49 | if !isValid(index: index) { 50 | throw PitchError.invalidPitchIndex 51 | } 52 | } 53 | 54 | public static func isValid(octave: Int) -> Bool { 55 | octaveBounds.contains(octave) 56 | } 57 | 58 | public static func validate(octave: Int) throws { 59 | if !isValid(octave: octave) { 60 | throw PitchError.invalidOctave 61 | } 62 | } 63 | 64 | // MARK: - Pitch Notations 65 | 66 | public static func frequency(forIndex index: Int) throws -> Double { 67 | try validate(index: index) 68 | 69 | let count = letters.count 70 | let power = Double(index) / Double(count) 71 | 72 | return pow(2, power) * Standard.frequency 73 | } 74 | 75 | public static func letter(forIndex index: Int) throws -> Note.Letter { 76 | try validate(index: index) 77 | 78 | let count = letters.count 79 | var lettersIndex = index < 0 80 | ? count - abs(index) % count 81 | : index % count 82 | 83 | if lettersIndex == 12 { 84 | lettersIndex = 0 85 | } 86 | 87 | guard (0 ..< letters.count) ~= lettersIndex else { 88 | throw PitchError.invalidPitchIndex 89 | } 90 | 91 | return letters[lettersIndex] 92 | } 93 | 94 | public static func octave(forIndex index: Int) throws -> Int { 95 | try validate(index: index) 96 | 97 | let count = letters.count 98 | let resNegativeIndex = Standard.octave - (abs(index) + 2) / count 99 | let resPositiveIndex = Standard.octave + (index + 9) / count 100 | 101 | return index < 0 102 | ? resNegativeIndex 103 | : resPositiveIndex 104 | } 105 | 106 | // MARK: - Pitch Index 107 | 108 | public static func index(forFrequency frequency: Double) throws -> Int { 109 | try FrequencyValidator.validate(frequency: frequency) 110 | let count = Double(letters.count) 111 | 112 | return Int(round(count * log2(frequency / Standard.frequency))) 113 | } 114 | 115 | public static func index(forLetter letter: Note.Letter, octave: Int) throws -> Int { 116 | try validate(octave: octave) 117 | 118 | let count = letters.count 119 | let letterIndex = letters.firstIndex(of: letter) ?? 0 120 | let offset = letterIndex < 3 ? 0 : count 121 | 122 | return letterIndex + count * (octave - Standard.octave) - offset 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/Tuna/SignalTracking/Units/InputSignalTracker.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | public enum InputSignalTrackerError: Error { 4 | case inputNodeMissing 5 | } 6 | 7 | class InputSignalTracker: SignalTracker { 8 | weak var delegate: SignalTrackerDelegate? 9 | var levelThreshold: Float? 10 | 11 | private let bufferSize: AVAudioFrameCount 12 | private var audioChannel: AVCaptureAudioChannel? 13 | private let captureSession = AVCaptureSession() 14 | private var audioEngine: AVAudioEngine? 15 | #if os(iOS) 16 | private let session = AVAudioSession.sharedInstance() 17 | #endif 18 | private let bus = 0 19 | 20 | /// The peak level of the signal 21 | var peakLevel: Float? { 22 | audioChannel?.peakHoldLevel 23 | } 24 | 25 | /// The average level of the signal 26 | var averageLevel: Float? { 27 | audioChannel?.averagePowerLevel 28 | } 29 | 30 | /// The tracker mode 31 | var mode: SignalTrackerMode { 32 | .record 33 | } 34 | 35 | // MARK: - Initialization 36 | 37 | required init(bufferSize: AVAudioFrameCount = 2048, delegate: SignalTrackerDelegate? = nil) { 38 | self.bufferSize = bufferSize 39 | self.delegate = delegate 40 | setupAudio() 41 | } 42 | 43 | // MARK: - Tracking 44 | 45 | func start() throws { 46 | 47 | #if os(iOS) 48 | try session.setCategory(.playAndRecord) 49 | 50 | // check input type 51 | let outputs = session.currentRoute.outputs 52 | if !outputs.isEmpty { 53 | for output in outputs { 54 | switch output.portType { 55 | case .headphones: 56 | // input from default (headphones) 57 | try session.overrideOutputAudioPort(.none) 58 | default: 59 | // input from speaker if port is not headphones 60 | try session.overrideOutputAudioPort(.speaker) 61 | } 62 | } 63 | } 64 | #endif 65 | 66 | audioEngine = AVAudioEngine() 67 | 68 | guard let inputNode = audioEngine?.inputNode else { 69 | throw InputSignalTrackerError.inputNodeMissing 70 | } 71 | 72 | let format = inputNode.outputFormat(forBus: bus) 73 | 74 | inputNode.installTap(onBus: bus, bufferSize: bufferSize, format: format) { buffer, time in 75 | guard let averageLevel = self.averageLevel else { return } 76 | 77 | let levelThreshold = self.levelThreshold ?? -1000000.0 78 | 79 | DispatchQueue.main.async { 80 | if averageLevel > levelThreshold { 81 | self.delegate?.signalTracker(self, didReceiveBuffer: buffer, atTime: time) 82 | } else { 83 | self.delegate?.signalTrackerWentBelowLevelThreshold(self) 84 | } 85 | } 86 | } 87 | 88 | try audioEngine?.start() 89 | captureSession.startRunning() 90 | 91 | guard captureSession.isRunning == true else { 92 | throw InputSignalTrackerError.inputNodeMissing 93 | } 94 | } 95 | 96 | func stop() { 97 | guard audioEngine != nil else { 98 | return 99 | } 100 | 101 | audioEngine?.stop() 102 | audioEngine?.reset() 103 | audioEngine = nil 104 | captureSession.stopRunning() 105 | } 106 | 107 | private func setupAudio() { 108 | do { 109 | let audioDevice = AVCaptureDevice.default(for: AVMediaType.audio) 110 | let audioCaptureInput = try AVCaptureDeviceInput(device: audioDevice!) 111 | let audioOutput = AVCaptureAudioDataOutput() 112 | 113 | captureSession.addInput(audioCaptureInput) 114 | captureSession.addOutput(audioOutput) 115 | 116 | let connection = audioOutput.connections[0] 117 | audioChannel = connection.audioChannels[0] 118 | } catch { 119 | debugPrint(error) 120 | } 121 | } 122 | } 123 | 124 | #if canImport(Combine) 125 | import Combine 126 | 127 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 128 | class SignalTrackerPublisher { 129 | let subject = PassthroughSubject<(AVAudioPCMBuffer, AVAudioTime), Error>() 130 | } 131 | 132 | @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 133 | extension SignalTrackerPublisher: SignalTrackerDelegate { 134 | func signalTracker(_ signalTracker: SignalTracker, didReceiveBuffer buffer: AVAudioPCMBuffer, atTime time: AVAudioTime) { 135 | subject.send((buffer, time)) 136 | } 137 | 138 | func signalTrackerWentBelowLevelThreshold(_ signalTracker: SignalTracker) { 139 | 140 | } 141 | } 142 | 143 | #endif 144 | -------------------------------------------------------------------------------- /Sources/Tuna/PitchEngine.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | #if canImport(UIKit) 5 | import UIKit 6 | #endif 7 | 8 | public protocol PitchEngineDelegate: class { 9 | func pitchEngine(_ pitchEngine: PitchEngine, didReceive result: Result) 10 | } 11 | 12 | public typealias PitchEngineCallback = (Result) -> Void 13 | 14 | public class PitchEngine { 15 | 16 | public enum Error: Swift.Error { 17 | case recordPermissionDenied 18 | case levelBelowThreshold 19 | } 20 | 21 | public let bufferSize: AVAudioFrameCount 22 | public private(set) var active = false 23 | public weak var delegate: PitchEngineDelegate? 24 | private var callback: PitchEngineCallback? 25 | 26 | private let estimator: Estimator 27 | private let signalTracker: SignalTracker 28 | private let queue = DispatchQueue(label: "TunaQueue", attributes: []) 29 | 30 | public var mode: SignalTrackerMode { 31 | return signalTracker.mode 32 | } 33 | 34 | public var levelThreshold: Float? { 35 | get { 36 | self.signalTracker.levelThreshold 37 | } 38 | set { 39 | self.signalTracker.levelThreshold = newValue 40 | } 41 | } 42 | 43 | public var signalLevel: Float { 44 | signalTracker.averageLevel ?? 0.0 45 | } 46 | 47 | // MARK: - Initialization 48 | 49 | public init(bufferSize: AVAudioFrameCount = 4096, estimationStrategy: EstimationStrategy = .yin, audioUrl: URL? = nil, signalTracker: SignalTracker? = nil, delegate: PitchEngineDelegate? = nil, callback: PitchEngineCallback? = nil) { 50 | 51 | self.bufferSize = bufferSize 52 | self.estimator = estimationStrategy.estimator 53 | 54 | if let signalTracker = signalTracker { 55 | self.signalTracker = signalTracker 56 | } else { 57 | if let audioUrl = audioUrl { 58 | self.signalTracker = OutputSignalTracker(audioUrl: audioUrl, bufferSize: bufferSize) 59 | } else { 60 | self.signalTracker = InputSignalTracker(bufferSize: bufferSize) 61 | } 62 | } 63 | 64 | 65 | self.signalTracker.delegate = self 66 | self.delegate = delegate 67 | self.callback = callback 68 | } 69 | 70 | // MARK: - Processing 71 | 72 | public func start() { 73 | 74 | guard mode == .playback else { 75 | activate() 76 | return 77 | } 78 | 79 | #if os(iOS) 80 | let audioSession = AVAudioSession.sharedInstance() 81 | 82 | switch audioSession.recordPermission { 83 | 84 | case .granted: 85 | activate() 86 | 87 | case .denied: 88 | DispatchQueue.main.async { 89 | if let settingsURL = URL(string: UIApplication.openSettingsURLString) { 90 | UIApplication.shared.openURL(settingsURL) 91 | } 92 | } 93 | 94 | case .undetermined: 95 | AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in 96 | guard let self = self else { return } 97 | 98 | guard granted else { 99 | self.delegate?.pitchEngine(self, didReceive: .failure(Error.recordPermissionDenied)) 100 | self.callback?(.failure(Error.recordPermissionDenied)) 101 | return 102 | } 103 | 104 | DispatchQueue.main.async { 105 | self.activate() 106 | } 107 | } 108 | 109 | @unknown default: 110 | break 111 | } 112 | #endif 113 | } 114 | 115 | public func stop() { 116 | signalTracker.stop() 117 | active = false 118 | } 119 | 120 | func activate() { 121 | do { 122 | try signalTracker.start() 123 | active = true 124 | } catch { 125 | delegate?.pitchEngine(self, didReceive: .failure(error)) 126 | callback?(.failure(error)) 127 | } 128 | } 129 | } 130 | 131 | // MARK: - SignalTrackingDelegate 132 | 133 | extension PitchEngine: SignalTrackerDelegate { 134 | 135 | public func signalTracker(_ signalTracker: SignalTracker, didReceiveBuffer buffer: AVAudioPCMBuffer, atTime time: AVAudioTime) { 136 | queue.async { [weak self] in 137 | guard let self = self else { return } 138 | 139 | do { 140 | let transformedBuffer = try self.estimator.transformer.transform(buffer: buffer) 141 | let frequency = try self.estimator.estimateFrequency(sampleRate: Float(time.sampleRate), buffer: transformedBuffer) 142 | let pitch = try Pitch(frequency: Double(frequency)) 143 | 144 | DispatchQueue.main.async { [weak self] in 145 | guard let self = self else { return } 146 | self.delegate?.pitchEngine(self, didReceive: .success(pitch)) 147 | self.callback?(.success(pitch)) 148 | } 149 | } catch { 150 | DispatchQueue.main.async { [weak self] in 151 | guard let self = self else { return } 152 | self.delegate?.pitchEngine(self, didReceive: .failure(error)) 153 | self.callback?(.failure(error)) 154 | } 155 | } 156 | } 157 | } 158 | 159 | public func signalTrackerWentBelowLevelThreshold(_ signalTracker: SignalTracker) { 160 | DispatchQueue.main.async { 161 | self.delegate?.pitchEngine(self, didReceive: .failure(Error.levelBelowThreshold)) 162 | self.callback?(.failure(Error.levelBelowThreshold)) 163 | } 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /Sources/Tuna/Transform/Strategies/FFTTransformer.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Accelerate 3 | 4 | /// An FFT Transformer 5 | struct FFTTransformer: Transformer { 6 | 7 | func transform(buffer: AVAudioPCMBuffer) throws -> Buffer { 8 | let frameCount = Double(buffer.frameLength) 9 | let log2n = UInt(round(log2(frameCount))) 10 | let bufferSizePOT = Int(1 << log2n) 11 | let inputCount = bufferSizePOT / 2 12 | let fftSetup = vDSP_create_fftsetup(log2n, FFTRadix(kFFTRadix2)) 13 | 14 | var realp = [Float](repeating: 0, count: inputCount) 15 | var imagp = [Float](repeating: 0, count: inputCount) 16 | 17 | let windowSize = bufferSizePOT 18 | var transferBuffer = [Float](repeating: 0, count: windowSize) 19 | var window = [Float](repeating: 0, count: windowSize) 20 | 21 | var normalizedMagnitudes = [Float](repeating: 0.0, count: inputCount) 22 | 23 | realp.withUnsafeMutableBufferPointer { realBP in 24 | imagp.withUnsafeMutableBufferPointer { imaginaryBP in 25 | var output = DSPSplitComplex(realp: realBP.baseAddress!, imagp: imaginaryBP.baseAddress!) 26 | 27 | // Hann windowing to reduce the frequency leakage 28 | vDSP_hann_window(&window, vDSP_Length(windowSize), Int32(vDSP_HANN_NORM)) 29 | vDSP_vmul((buffer.floatChannelData?.pointee)!, 1, window, 1, &transferBuffer, 1, vDSP_Length(windowSize)) 30 | 31 | // Transforming the [Float] buffer into a UnsafePointer object for the vDSP_ctoz method 32 | // And then pack the input into the complex buffer (output) 33 | let temp = UnsafePointer(transferBuffer) 34 | temp.withMemoryRebound(to: DSPComplex.self, capacity: transferBuffer.count) { typeConvertedTransferBuffer in 35 | vDSP_ctoz(typeConvertedTransferBuffer, 2, &output, 1, vDSP_Length(inputCount)) 36 | } 37 | 38 | // Perform the FFT 39 | vDSP_fft_zrip(fftSetup!, &output, 1, log2n, FFTDirection(FFT_FORWARD)) 40 | 41 | var magnitudes = [Float](repeating: 0.0, count: inputCount) 42 | vDSP_zvmags(&output, 1, &magnitudes, 1, vDSP_Length(inputCount)) 43 | 44 | // Normalising 45 | vDSP_vsmul(sqrtq(magnitudes), 1, [2.0 / Float(inputCount)], &normalizedMagnitudes, 1, vDSP_Length(inputCount)) 46 | } 47 | } 48 | 49 | let buffer = Buffer(elements: normalizedMagnitudes) 50 | 51 | vDSP_destroy_fftsetup(fftSetup) 52 | 53 | return buffer 54 | } 55 | 56 | @available(iOS 13.0, OSX 10.15, *) 57 | func fft(buffer: AVAudioPCMBuffer) throws -> Buffer { 58 | let frameCount = buffer.frameLength 59 | let log2n = vDSP_Length(log2(Float(frameCount))) 60 | let halfN = Int(frameCount / 2) 61 | var forwardInputReal = [Float](repeating: 0, count: halfN) 62 | var forwardInputImag = [Float](repeating: 0, count: halfN) 63 | var forwardOutputReal = [Float](repeating: 0, count: halfN) 64 | var forwardOutputImag = [Float](repeating: 0, count: halfN) 65 | 66 | let data = buffer.floatChannelData?[0] 67 | let arrayOfData = Array(UnsafeBufferPointer(start: data, count: Int(buffer.frameLength))) 68 | 69 | let tau: Float = .pi * 2 70 | let signal: [Float] = (0 ... frameCount).map { index in 71 | arrayOfData.reduce(0) { accumulator, frequency in 72 | let normalizedIndex = Float(index) / Float(frameCount) 73 | return accumulator + sin(normalizedIndex * frequency * tau) 74 | } 75 | } 76 | 77 | guard let fftSetUp = vDSP.FFT(log2n: log2n, radix: .radix2, ofType: DSPSplitComplex.self) else { 78 | fatalError("Can't create FFT Setup.") 79 | } 80 | 81 | forwardInputReal.withUnsafeMutableBufferPointer { forwardInputRealPtr in 82 | forwardInputImag.withUnsafeMutableBufferPointer { forwardInputImagPtr in 83 | forwardOutputReal.withUnsafeMutableBufferPointer { forwardOutputRealPtr in 84 | forwardOutputImag.withUnsafeMutableBufferPointer { forwardOutputImagPtr in 85 | 86 | // 1: Create a `DSPSplitComplex` to contain the signal. 87 | var forwardInput = DSPSplitComplex(realp: forwardInputRealPtr.baseAddress!, imagp: forwardInputImagPtr.baseAddress!) 88 | 89 | // 2: Convert the real values in `signal` to complex numbers. 90 | signal.withUnsafeBytes { 91 | vDSP.convert(interleavedComplexVector: [DSPComplex]($0.bindMemory(to: DSPComplex.self)), toSplitComplexVector: &forwardInput) 92 | } 93 | 94 | // 3: Create a `DSPSplitComplex` to receive the FFT result. 95 | var forwardOutput = DSPSplitComplex(realp: forwardOutputRealPtr.baseAddress!, imagp: forwardOutputImagPtr.baseAddress!) 96 | 97 | // 4: Perform the forward FFT. 98 | fftSetUp.forward(input: forwardInput, output: &forwardOutput) 99 | } 100 | } 101 | } 102 | } 103 | 104 | return Buffer(elements: forwardOutputReal) 105 | } 106 | 107 | // MARK: - Helpers 108 | 109 | /// Calculate the square roots of the elements in a [Float] 110 | /// - Parameter x: The float array 111 | /// - Returns: An array with the square roots 112 | func sqrtq(_ x: [Float]) -> [Float] { 113 | var results = [Float](repeating: 0.0, count: x.count) 114 | vvsqrtf(&results, x, [Int32(x.count)]) 115 | return results 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Sources/Tuna/Utilities/YINUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YINUtil.swift 3 | // Beethoven 4 | // 5 | // Created by Guillaume Laurent on 09/10/16. 6 | // Adapted from https://code.soundsoftware.ac.uk/projects/pyin/repository 7 | // by Matthias Mauch, Centre for Digital Music, Queen Mary, University of London. 8 | // 9 | // 10 | 11 | import Foundation 12 | import Accelerate 13 | 14 | struct YINUtil { 15 | 16 | // Slow and eats a lot of CPU, but working 17 | static func difference2(buffer: [Float]) -> [Float] { 18 | let bufferHalfCount = buffer.count / 2 19 | var resultBuffer = [Float](repeating:0.0, count:bufferHalfCount) 20 | 21 | for tau in 0 ..< bufferHalfCount { 22 | for i in 0 ..< bufferHalfCount { 23 | let delta = buffer[i] - buffer[i + tau] 24 | resultBuffer[tau] += delta * delta 25 | } 26 | } 27 | 28 | return resultBuffer 29 | } 30 | 31 | // Accelerated version of difference2 - 32 | // Instruments shows roughly around 22% CPU usage, compared to 95% for difference2 33 | static func differenceA(buffer: [Float]) -> [Float] { 34 | let bufferHalfCount = buffer.count / 2 35 | var resultBuffer = [Float](repeating:0.0, count: bufferHalfCount) 36 | var tempBuffer = [Float](repeating:0.0, count: bufferHalfCount) 37 | var tempBufferSq = [Float](repeating:0.0, count: bufferHalfCount) 38 | let len = vDSP_Length(bufferHalfCount) 39 | var vSum: Float = 0.0 40 | 41 | for tau in 0 ..< bufferHalfCount { 42 | let bufferTau = UnsafePointer(buffer).advanced(by: tau) 43 | // do a diff of buffer with itself at tau offset 44 | vDSP_vsub(buffer, 1, bufferTau, 1, &tempBuffer, 1, len) 45 | // square each value of the diff vector 46 | vDSP_vsq(tempBuffer, 1, &tempBufferSq, 1, len) 47 | // sum the squared values into vSum 48 | vDSP_sve(tempBufferSq, 1, &vSum, len) 49 | // store that in the result buffer 50 | resultBuffer[tau] = vSum 51 | } 52 | 53 | return resultBuffer 54 | } 55 | 56 | // Supposedly faster and less CPU consuming, but doesn't work, must be because I missed something when porting it from 57 | // https://code.soundsoftware.ac.uk/projects/pyin/repository but I don't know what 58 | // 59 | // Kept for reference only. 60 | // swiftlint:disable function_body_length 61 | static func difference_broken_do_not_use(buffer: [Float]) -> [Float] { 62 | let frameSize = buffer.count 63 | let yinBufferSize = frameSize / 2 64 | 65 | // power terms calculation 66 | var powerTerms = [Float](repeating:0, count:yinBufferSize) 67 | 68 | _ = { (res: Float, element: Float) -> Float in 69 | res + element * element 70 | } 71 | 72 | var powerTermFirstElement: Float = 0.0 73 | for j in 0 ..< yinBufferSize { 74 | powerTermFirstElement += buffer[j] * buffer[j] 75 | } 76 | 77 | powerTerms[0] = powerTermFirstElement 78 | 79 | for tau in 1 ..< yinBufferSize { 80 | let v = powerTerms[tau - 1] 81 | let v1 = buffer[tau - 1] * buffer[tau - 1] 82 | let v2 = buffer[tau + yinBufferSize] * buffer[tau + yinBufferSize] 83 | let newV = v - v1 + v2 84 | 85 | powerTerms[tau] = newV 86 | } 87 | 88 | let log2n = UInt(round(log2(Double(buffer.count)))) 89 | let bufferSizePOT = Int(1 << log2n) 90 | let inputCount = bufferSizePOT / 2 91 | let fftSetup = vDSP_create_fftsetup(log2n, Int32(kFFTRadix2)) 92 | var audioRealp = [Float](repeating: 0, count: inputCount) 93 | var audioImagp = [Float](repeating: 0, count: inputCount) 94 | var audioTransformedComplex = DSPSplitComplex(realp: &audioRealp, imagp: &audioImagp) 95 | 96 | let temp = UnsafePointer(buffer) 97 | 98 | temp.withMemoryRebound(to: DSPComplex.self, capacity: buffer.count) { (typeConvertedTransferBuffer) -> Void in 99 | vDSP_ctoz(typeConvertedTransferBuffer, 2, &audioTransformedComplex, 1, vDSP_Length(inputCount)) 100 | } 101 | 102 | // YIN-STYLE AUTOCORRELATION via FFT 103 | // 1. data 104 | vDSP_fft_zrip(fftSetup!, &audioTransformedComplex, 1, log2n, FFTDirection(FFT_FORWARD)) 105 | 106 | var kernel = [Float](repeating: 0, count: frameSize) 107 | 108 | // 2. half of the data, disguised as a convolution kernel 109 | // 110 | for j in 0 ..< yinBufferSize { 111 | kernel[j] = buffer[yinBufferSize - 1 - j] 112 | } 113 | // for j in yinBufferSize ..< frameSize { 114 | // kernel[j] = 0.0 115 | // } 116 | 117 | var kernelRealp = [Float](repeating: 0, count: frameSize) 118 | var kernelImagp = [Float](repeating: 0, count: frameSize) 119 | var kernelTransformedComplex = DSPSplitComplex(realp: &kernelRealp, imagp: &kernelImagp) 120 | 121 | let ktemp = UnsafePointer(kernel) 122 | 123 | ktemp.withMemoryRebound(to: DSPComplex.self, capacity: kernel.count) { (typeConvertedTransferBuffer) -> Void in 124 | vDSP_ctoz(typeConvertedTransferBuffer, 2, &kernelTransformedComplex, 1, vDSP_Length(inputCount)) 125 | } 126 | 127 | vDSP_fft_zrip(fftSetup!, &kernelTransformedComplex, 1, log2n, FFTDirection(FFT_FORWARD)) 128 | 129 | var yinStyleACFRealp = [Float](repeating: 0, count: frameSize) 130 | var yinStyleACFImagp = [Float](repeating: 0, count: frameSize) 131 | var yinStyleACFComplex = DSPSplitComplex(realp: &yinStyleACFRealp, imagp: &yinStyleACFImagp) 132 | 133 | for j in 0 ..< inputCount { 134 | yinStyleACFRealp[j] = audioRealp[j] * kernelRealp[j] - audioImagp[j] * kernelImagp[j] 135 | yinStyleACFImagp[j] = audioRealp[j] * kernelImagp[j] + audioImagp[j] * kernelRealp[j] 136 | } 137 | 138 | vDSP_fft_zrip(fftSetup!, &yinStyleACFComplex, 1, log2n, FFTDirection(FFT_INVERSE)) 139 | 140 | var resultYinBuffer = [Float](repeating:0.0, count: yinBufferSize) 141 | 142 | for j in 0 ..< yinBufferSize { 143 | resultYinBuffer[j] = powerTerms[0] + powerTerms[j] - 2 * yinStyleACFRealp[j + yinBufferSize - 1] 144 | } 145 | 146 | return resultYinBuffer 147 | } 148 | 149 | static func cumulativeDifference(yinBuffer: inout [Float]) { 150 | yinBuffer[0] = 1.0 151 | 152 | var runningSum: Float = 0.0 153 | 154 | for tau in 1 ..< yinBuffer.count { 155 | runningSum += yinBuffer[tau] 156 | 157 | if runningSum == 0 { 158 | yinBuffer[tau] = 1 159 | } else { 160 | yinBuffer[tau] *= Float(tau) / runningSum 161 | } 162 | } 163 | } 164 | 165 | static func absoluteThreshold(yinBuffer: [Float], withThreshold threshold: Float) -> Int { 166 | var tau = 2 167 | var minTau = 0 168 | var minVal: Float = 1000.0 169 | 170 | while tau < yinBuffer.count { 171 | if yinBuffer[tau] < threshold { 172 | while (tau + 1) < yinBuffer.count && yinBuffer[tau + 1] < yinBuffer[tau] { 173 | tau += 1 174 | } 175 | return tau 176 | } else { 177 | if yinBuffer[tau] < minVal { 178 | minVal = yinBuffer[tau] 179 | minTau = tau 180 | } 181 | } 182 | tau += 1 183 | } 184 | 185 | if minTau > 0 { 186 | return -minTau 187 | } 188 | 189 | return 0 190 | } 191 | 192 | static func parabolicInterpolation(yinBuffer: [Float], tau: Int) -> Float { 193 | guard tau != yinBuffer.count else { 194 | return Float(tau) 195 | } 196 | 197 | var betterTau: Float = 0.0 198 | 199 | if tau > 0 && tau < yinBuffer.count - 1 { 200 | let s0 = yinBuffer[tau - 1] 201 | let s1 = yinBuffer[tau] 202 | let s2 = yinBuffer[tau + 1] 203 | 204 | var adjustment = (s2 - s0) / (2.0 * (2.0 * s1 - s2 - s0)) 205 | 206 | if abs(adjustment) > 1 { 207 | adjustment = 0 208 | } 209 | 210 | betterTau = Float(tau) + adjustment 211 | } else { 212 | betterTau = Float(tau) 213 | } 214 | 215 | return abs(betterTau) 216 | } 217 | 218 | static func sumSquare(yinBuffer: [Float], start: Int, end: Int) -> Float { 219 | var out: Float = 0.0 220 | 221 | for i in start ..< end { 222 | out += yinBuffer[i] * yinBuffer[i] 223 | } 224 | 225 | return out 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tuna hero](https://user-images.githubusercontent.com/156458/118390274-d8663c00-b636-11eb-9088-f2d5eaef287d.jpg) 2 | 3 | --- 4 | 5 | **Disclaimer** 6 | 7 | This project is based on [Beethoven](https://github.com/vadymmarkov/Beethoven) & [Pitchy](https://github.com/vadymmarkov/Pitchy), two excellent projects by [Vadym Markov](https://github.com/vadymmarkov) that are unfortunatelly not so actively developed any more. The code have been consolidated, modernized for Swift5, refactored and documented. I have also removed dependencies and added support for macOS. The heart of the libraries is the same and for anyone that used any of these libraries the transition should be fairly easy. 8 | 9 | --- 10 | 11 | ## Key features 12 | - Get lower, higher and closest pitch offsets from a specified frequency. 13 | - Get an acoustic wave with wavelength, period and harmonics. 14 | - Create a note from a pitch index, frequency or a letter with octave number. 15 | - Calculate a frequency, note letter and octave from a pitch index 16 | - Find a pitch index from a specified frequency or a note letter with octave. 17 | - Convert a frequency to wavelength and vice versa. 18 | - Convert a wavelength to time period and vice versa. 19 | - Audio signal tracking with `AVAudioEngine` and audio nodes. 20 | - Pre-processing of audio buffer by one of the available "transformers". 21 | - Pitch estimation. 22 | 23 | ## Index 24 | * Pitch: 25 | * [Pitch](#pitch) 26 | * [Acoustic wave](#acoustic-wave) 27 | * [Note](#note) 28 | * [Calculators](#calculators) 29 | * [FrequencyValidator](#frequencyvalidator) 30 | * [Error handling](#pitch-error-handling) 31 | 32 | * PitchEngine: 33 | * [Pitch engine](#pitch-engine) 34 | * [Signal tracking](#signal-tracking) 35 | * [Transform](#transform) 36 | * [Estimation](#estimation) 37 | * [Error handling](#pitch-engine-error-handling) 38 | * [Pitch detection specifics](#pitch-detection-specifics) 39 | 40 | * [Authors](#authors) 41 | * [License](#license) 42 | 43 | --- 44 | 45 | ### Pitch 46 | Create `Pitch` struct with a specified frequency to get lower, higher and 47 | closest pitch offsets: 48 | 49 | ```swift 50 | do { 51 | // Frequency = 445 Hz 52 | let pitch = try Pitch(frequency: 445.0) 53 | let pitchOffsets = pitch.offsets 54 | 55 | print(pitchOffsets.lower.frequency) // 5 Hz 56 | print(pitchOffsets.lower.percentage) // 19.1% 57 | print(pitchOffsets.lower.note.index) // 0 58 | print(pitchOffsets.lower.cents) // 19.56 59 | 60 | print(pitchOffsets.higher.frequency) // -21.164 Hz 61 | print(pitchOffsets.higher.percentage) // -80.9% 62 | print(pitchOffsets.higher.note.index) // 1 63 | print(pitchOffsets.higher.cents) // -80.4338 64 | 65 | print(pitchOffsets.closest.note) // "A4" 66 | 67 | // You could also use acoustic wave 68 | print(pitch.wave.wavelength) // 0.7795 meters 69 | } catch { 70 | // Handle errors 71 | } 72 | ``` 73 | 74 | 75 | ### Acoustic wave 76 | Get an acoustic wave with wavelength, period and harmonics. 77 | 78 | ```swift 79 | do { 80 | // AcousticWave(wavelength: 0.7795) 81 | // AcousticWave(period: 0.00227259) 82 | let wave = try AcousticWave(frequency: 440.0) 83 | 84 | print(wave.frequency) // 440 Hz 85 | print(wave.wavelength) // 0.7795 meters 86 | print(wave.period) // 0.00227259 s 87 | print(wave.harmonics[0]) // 440 Hz 88 | print(wave.harmonics[1]) // 880 Hz 89 | } catch { 90 | // Handle errors 91 | } 92 | ``` 93 | 94 | 95 | ### Note 96 | Note could be created with a corresponding frequency, letter + octave number or 97 | a pitch index. 98 | 99 | ```swift 100 | do { 101 | // Note(frequency: 261.626) 102 | // Note(letter: .C, octave: 4) 103 | let note = try Note(index: -9) 104 | 105 | print(note.index) // -9 106 | print(note.letter) // .C 107 | print(note.octave) // 4 108 | print(note.frequency) // 261.626 Hz 109 | print(note) // "C4" 110 | print(try note.lower()) // "B3" 111 | print(try note.higher()) // "C#4" 112 | } catch { 113 | // Handle errors 114 | } 115 | ``` 116 | 117 | 118 | ### Calculators 119 | Calculators are used in the initialization of `Pitch`, `AcousticWave` 120 | and `Note`, but also are included in the public API. 121 | 122 | ```swift 123 | do { 124 | // PitchCalculator 125 | let pitchOffsets = try PitchCalculator.offsets(445.0) 126 | let cents = try PitchCalculator.cents(frequency1: 440.0, frequency2: 440.0) // 19.56 127 | 128 | // NoteCalculator 129 | let frequency1 = try NoteCalculator.frequency(forIndex: 0) // 440.0 Hz 130 | let letter = try NoteCalculator.letter(forIndex: 0) // .A 131 | let octave = try NoteCalculator.octave(forIndex: 0) // 4 132 | let index1 = try NoteCalculator.index(forFrequency: 440.0) // 0 133 | let index2 = try NoteCalculator.index(forLetter: .A, octave: 4) // 0 134 | 135 | // WaveCalculator 136 | let f = try WaveCalculator.frequency(forWavelength: 0.7795) // 440.0 Hz 137 | let wl1 = try WaveCalculator.wavelength(forFrequency: 440.0) // 0.7795 meters 138 | let wl2 = try WaveCalculator.wavelength(forPeriod: 0.00227259) // 0.7795 meters 139 | let period = try WaveCalculator.period(forWavelength: 0.7795) // 0.00227259 s 140 | } catch { 141 | // Handle errors 142 | } 143 | ``` 144 | 145 | 146 | ### FrequencyValidator 147 | With a help of `FrequencyValidator` it's possible to adjust the range of frequencies that are used for validations in all calculations: 148 | 149 | ```swift 150 | FrequencyValidator.range = 20.0 ... 4190.0 // This btw is the default range 151 | ``` 152 | 153 | 154 | ### Pitch error handling 155 | Almost everything is covered with tests, but it's important to pass valid 156 | values, such as frequencies and pitch indexes. That's why there is a list of errors that should be handled properly. 157 | 158 | ```swift 159 | enum PitchError: Error { 160 | case invalidFrequency 161 | case invalidWavelength 162 | case invalidPeriod 163 | case invalidPitchIndex 164 | case invalidOctave 165 | } 166 | ``` 167 | 168 | --- 169 | 170 | ### Pitch engine 171 | `PitchEngine` is the main class you are going to work with to find the pitch. 172 | It can be instantiated with a delegate, a closure callback or both: 173 | 174 | ```swift 175 | let pitchEngine = PitchEngine(delegate: delegate) 176 | ``` 177 | 178 | or 179 | 180 | ```swift 181 | let pitchEngine = PitchEngine { result in 182 | 183 | switch result { 184 | case .success(let pitch): 185 | // Handle the reported pitch 186 | 187 | case .failure(let error): 188 | // Handle the error 189 | 190 | switch error { 191 | case PitchEngine.Error.levelBelowThreshold: break 192 | case PitchEngine.Error.recordPermissionDenied: break 193 | 194 | case PitchError.invalidFrequency: break 195 | case PitchError.invalidWavelength: break 196 | case PitchError.invalidPeriod: break 197 | case PitchError.invalidPitchIndex: break 198 | case PitchError.invalidOctave: break 199 | default: break 200 | } 201 | } 202 | 203 | } 204 | ``` 205 | 206 | the initializers have also the following optional parameters: 207 | 208 | ```swift 209 | bufferSize: AVAudioFrameCount = 4096 210 | estimationStrategy: EstimationStrategy = .yin 211 | audioUrl: URL? = nil 212 | signalTracker: SignalTracker? = nil 213 | ``` 214 | 215 | `PitchEngineDelegate` have a single requirement and reports back a `Result` (just like the callback): 216 | 217 | ```swift 218 | func pitchEngine(_ pitchEngine: PitchEngine, didReceive result: Result) 219 | ``` 220 | 221 | For reference the full init signature is: 222 | 223 | ```swift 224 | public init(bufferSize: AVAudioFrameCount = 4096, 225 | estimationStrategy: EstimationStrategy = .yin, 226 | audioUrl: URL? = nil, 227 | signalTracker: SignalTracker? = nil, 228 | delegate: PitchEngineDelegate? = nil, 229 | callback: PitchEngineCallback? = nil) 230 | ``` 231 | 232 | It should be noted that both reporting mechanisms are conveniently called in the main queue, since you probably want to update your UI most of the time. 233 | 234 | To start or stop the pitch tracking process just use the corresponding `PitchEngine` methods: 235 | 236 | ```swift 237 | pitchEngine.start() 238 | pitchEngine.stop() 239 | ``` 240 | 241 | ### Signal tracking 242 | There are 2 signal tracking classes: 243 | - `InputSignalTracker` uses `AVAudioInputNode` to get an audio buffer from the 244 | recording input (microphone) in real-time. 245 | - `OutputSignalTracker` uses `AVAudioOutputNode` and `AVAudioFile` to play an 246 | audio file and get the audio buffer from the playback output. 247 | 248 | 249 | ### Transform 250 | Transform is the first step of audio processing where `AVAudioPCMBuffer` object 251 | is converted to an array of floating numbers. Also it's a place for different 252 | kind of optimizations. Then array is kept in the `elements` property of the 253 | internal `Buffer` struct, which also has optional `realElements` and 254 | `imagElements` properties that could be useful in the further calculations. 255 | 256 | There are 3 types of transformations at the moment: 257 | - [Fast Fourier transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform) 258 | - [YIN](http://recherche.ircam.fr/equipes/pcm/cheveign/pss/2002_JASA_YIN.pdf) 259 | - `Simple` conversion to use raw float channel data 260 | 261 | A new transform strategy could be easily added by implementing of `Transformer` 262 | protocol: 263 | 264 | ```swift 265 | public protocol Transformer { 266 | func transform(buffer: AVAudioPCMBuffer) -> Buffer 267 | } 268 | ``` 269 | 270 | ### Estimation 271 | A pitch detection algorithm (PDA) is an algorithm designed to estimate the pitch 272 | or fundamental frequency. Pitch is a psycho-acoustic phenomena, and it's 273 | important to choose the most suitable algorithm for your kind of input source, 274 | considering allowable error rate and needed performance. 275 | 276 | The list of available implemented algorithms: 277 | - `maxValue` - the index of the maximum value in the audio buffer used as a peak 278 | - `quadradic` - [Quadratic interpolation of spectral peaks](https://ccrma.stanford.edu/%7Ejos/sasp/Quadratic_Interpolation_Spectral_Peaks.html) 279 | - `barycentric` - [Barycentric correction](http://www.dspguru.com/dsp/howtos/how-to-interpolate-fft-peak) 280 | - `quinnsFirst` - [Quinn's First Estimator](http://www.dspguru.com/dsp/howtos/how-to-interpolate-fft-peak) 281 | - `quinnsSecond` - [Quinn's Second Estimator](http://www.dspguru.com/dsp/howtos/how-to-interpolate-fft-peak) 282 | - `jains` - [Jain's Method](http://www.dspguru.com/dsp/howtos/how-to-interpolate-fft-peak) 283 | - `hps` - [Harmonic Product Spectrum](http://musicweb.ucsd.edu/~trsmyth/analysis/Harmonic_Product_Spectrum.html) 284 | - `yin` - [YIN](http://recherche.ircam.fr/equipes/pcm/cheveign/pss/2002_JASA_YIN.pdf) 285 | 286 | A new estimation algorithm could be easily added by implementing of `Estimator` 287 | or `LocationEstimator` protocol: 288 | 289 | ```swift 290 | protocol Estimator { 291 | var transformer: Transformer { get } 292 | 293 | func estimateFrequency(sampleRate: Float, buffer: Buffer) throws -> Float 294 | func estimateFrequency(sampleRate: Float, location: Int, bufferCount: Int) -> Float 295 | } 296 | 297 | protocol LocationEstimator: Estimator { 298 | func estimateLocation(buffer: Buffer) throws -> Int 299 | } 300 | ``` 301 | 302 | Then it should be added to `EstimationStrategy` enum and in the `create` method 303 | of `EstimationFactory` struct. Normally, a buffer transformation should be 304 | performed in a separate struct or class to keep the code base more clean and 305 | readable. 306 | 307 | 308 | ### Pitch Engine error handling 309 | Pitch detection is not a trivial task due to some difficulties, such as attack 310 | transients, low and high frequencies. Also it's a real-time processing, so we 311 | are not protected against different kinds of errors. For this purpose there is a 312 | range of error types that should be handled properly. 313 | 314 | **Signal tracking errors** 315 | 316 | ```swift 317 | public enum InputSignalTrackerError: Error { 318 | case inputNodeMissing 319 | } 320 | ``` 321 | 322 | **Record permission errors** 323 | 324 | `PitchEngine` asks for `AVAudioSessionRecordPermission` on start, but if permission is denied it produces the corresponding error: 325 | 326 | ```swift 327 | public enum PitchEngineError: Error { 328 | case recordPermissionDenied 329 | } 330 | ``` 331 | 332 | **Pitch estimation errors** 333 | 334 | Some errors could occur during the process of pitch estimation: 335 | 336 | ```swift 337 | public enum EstimationError: Error { 338 | case emptyBuffer 339 | case unknownMaxIndex 340 | case unknownLocation 341 | case unknownFrequency 342 | } 343 | ``` 344 | 345 | ## Pitch detection specifics 346 | At the moment **Tuna** performs only a pitch detection of a monophonic recording. 347 | 348 | **Based on Stackoverflow** [answer](http://stackoverflow.com/a/14503090): 349 | 350 | > Pitch detection depends greatly on the musical content you want to work with. 351 | > Extracting the pitch of a monophonic recording (i.e. single instrument or voice) 352 | > is not the same as extracting the pitch of a single instrument from a polyphonic 353 | > mixture (e.g. extracting the pitch of the melody from a polyphonic recording). 354 | 355 | > For monophonic pitch extraction there are various algorithm that could be 356 | > implemented both in the time domain and frequency domain 357 | > ([Wikipedia](https://en.wikipedia.org/wiki/Pitch_detection_algorithm)). 358 | 359 | > However, neither will work well if you want to extract the melody from 360 | > polyphonic material. Melody extraction from polyphonic music is still a 361 | > research problem. 362 | 363 | ## Authors 364 | Vasilis Akoinoglou, alladinian@gmail.com 365 | Credit to original Author: Vadym Markov, markov.vadym@gmail.com 366 | 367 | ## License 368 | 369 | **Tuna** is available under the MIT license. See the LICENSE file for more info. 370 | -------------------------------------------------------------------------------- /Tests/TunaTests/PitchyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Tuna 3 | 4 | infix operator ≈ : ComparisonPrecedence 5 | 6 | fileprivate func ≈ (lhs: Double, rhs: (Double, Double)) -> Bool { 7 | abs(lhs - rhs.0) < rhs.1 8 | } 9 | 10 | final class PitchyTests: XCTestCase { 11 | 12 | func testFrequencyValidator() { 13 | XCTAssertFalse(FrequencyValidator.isValid(frequency: 5_000), "should be invalid if frequency is higher than maximum") 14 | XCTAssertFalse(FrequencyValidator.isValid(frequency: 10), "should be invalid if frequency is lower than minimum") 15 | XCTAssertFalse(FrequencyValidator.isValid(frequency: 0), "should be invalid if frequency is zero") 16 | XCTAssertTrue(FrequencyValidator.isValid(frequency: 440), "should be valid if frequency is within valid bounds") 17 | } 18 | 19 | func testNoteCalculator() { 20 | let notes = [ 21 | (index: 0, note: Note.Letter.A, octave: 4, frequency: 440.0), 22 | (index: 12, note: Note.Letter.A, octave: 5, frequency: 880.000), 23 | (index: 2, note: Note.Letter.B, octave: 4, frequency: 493.883), 24 | (index: -10, note: Note.Letter.B, octave: 3, frequency: 246.942), 25 | (index: -9, note: Note.Letter.C, octave: 4, frequency: 261.626), 26 | (index: -30, note: Note.Letter.DSharp, octave: 2, frequency: 77.7817), 27 | (index: 11, note: Note.Letter.GSharp, octave: 5, frequency: 830.609), 28 | (index: 29, note: Note.Letter.D, octave: 7, frequency: 2349.32) 29 | ] 30 | 31 | // Standard calculator base constant values 32 | XCTAssertEqual(NoteCalculator.Standard.frequency, 440) 33 | XCTAssertEqual(NoteCalculator.Standard.octave, 4) 34 | 35 | func indexBounds() { 36 | // Bounds based on min and max frequencies from the config 37 | let minimum = try! NoteCalculator.index(forFrequency: FrequencyValidator.minimumFrequency) 38 | let maximum = try! NoteCalculator.index(forFrequency: FrequencyValidator.maximumFrequency) 39 | let expected = (minimum: minimum, maximum: maximum) 40 | let result = NoteCalculator.indexBounds 41 | 42 | XCTAssertEqual(result.lowerBound, expected.minimum) 43 | XCTAssertEqual(result.upperBound, expected.maximum) 44 | } 45 | 46 | indexBounds() 47 | 48 | func octaveBounds() { 49 | // Bounds based on min and max frequencies from the config 50 | let bounds = NoteCalculator.indexBounds 51 | let minimum = try! NoteCalculator.octave(forIndex: bounds.lowerBound) 52 | let maximum = try! NoteCalculator.octave(forIndex: bounds.upperBound) 53 | let expected = (minimum: minimum, maximum: maximum) 54 | let result = NoteCalculator.octaveBounds 55 | 56 | XCTAssertEqual(result.lowerBound, expected.minimum) 57 | XCTAssertEqual(result.upperBound, expected.maximum) 58 | } 59 | 60 | octaveBounds() 61 | 62 | // Valid index 63 | XCTAssertFalse(NoteCalculator.isValid(index: 1_000), "is invalid if value is higher than maximum") 64 | XCTAssertFalse(NoteCalculator.isValid(index: -100), "is invalid if value is lower than minimum") 65 | XCTAssertTrue(NoteCalculator.isValid(index: 6), "is valid if value is within valid bounds") 66 | 67 | // Valid octave 68 | XCTAssertFalse(NoteCalculator.isValid(octave: 10), "is invalid if value is higher than maximum") 69 | XCTAssertFalse(NoteCalculator.isValid(octave: -1), "is invalid if value is lower than minimum") 70 | XCTAssertTrue(NoteCalculator.isValid(octave: 2), "is valid if value is within valid bounds") 71 | 72 | // Notes 73 | let letters = NoteCalculator.letters 74 | XCTAssertEqual(letters.count, 12) 75 | 76 | XCTAssertEqual(letters[0], Note.Letter.A) 77 | XCTAssertEqual(letters[1], Note.Letter.ASharp) 78 | XCTAssertEqual(letters[2], Note.Letter.B) 79 | XCTAssertEqual(letters[3], Note.Letter.C) 80 | XCTAssertEqual(letters[4], Note.Letter.CSharp) 81 | XCTAssertEqual(letters[5], Note.Letter.D) 82 | XCTAssertEqual(letters[6], Note.Letter.DSharp) 83 | XCTAssertEqual(letters[7], Note.Letter.E) 84 | XCTAssertEqual(letters[8], Note.Letter.F) 85 | XCTAssertEqual(letters[9], Note.Letter.FSharp) 86 | XCTAssertEqual(letters[10], Note.Letter.G) 87 | XCTAssertEqual(letters[11], Note.Letter.GSharp) 88 | 89 | for note in notes { 90 | XCTAssertTrue(try! NoteCalculator.frequency(forIndex: note.index) ≈ (note.frequency, 0.01)) 91 | XCTAssertEqual(try! NoteCalculator.letter(forIndex: note.index), note.note) 92 | XCTAssertEqual(try! NoteCalculator.octave(forIndex: note.index), note.octave) 93 | XCTAssertEqual(try! NoteCalculator.index(forFrequency: note.frequency), note.index) 94 | XCTAssertEqual(try! NoteCalculator.index(forLetter: note.note, octave: note.octave), note.index) 95 | } 96 | } 97 | 98 | func testPichCalculator() { 99 | let offsets = [ 100 | (frequency: 445.0, 101 | lower: Pitch.Offset(note: try! Note(index: 0), frequency: 5, percentage: 19.1, cents: 19.56), 102 | higher: Pitch.Offset(note: try! Note(index: 1), frequency: -21.164, percentage: -80.9, cents: -80.4338), 103 | closest: "A4" 104 | ), 105 | (frequency: 108.0, 106 | lower: Pitch.Offset(note: try! Note(index: -25), frequency: 4.174, percentage: 67.6, cents: 68.2333), 107 | higher: Pitch.Offset(note: try! Note(index: -24), frequency: -2, percentage: -32.39, cents: -31.76), 108 | closest: "A2" 109 | ) 110 | ] 111 | 112 | for offset in offsets { 113 | let result = try! PitchCalculator.offsets(forFrequency: offset.frequency) 114 | 115 | XCTAssertTrue(result.lower.frequency ≈ (offset.lower.frequency, 0.01)) 116 | XCTAssertTrue(result.lower.percentage ≈ (offset.lower.percentage, 0.1)) 117 | XCTAssertEqual(result.lower.note.index, offset.lower.note.index) 118 | XCTAssertTrue(result.lower.cents ≈ (offset.lower.cents, 0.1)) 119 | 120 | XCTAssertTrue(result.higher.frequency ≈ (offset.higher.frequency, 0.01)) 121 | XCTAssertTrue(result.higher.percentage ≈ (offset.higher.percentage, 0.1)) 122 | XCTAssertEqual(result.higher.note.index, offset.higher.note.index) 123 | XCTAssertTrue(result.higher.cents ≈ (offset.higher.cents, 0.1)) 124 | 125 | XCTAssertEqual(result.closest.note.description, offset.closest) 126 | } 127 | } 128 | 129 | func testWaveCalculator() { 130 | let waves = [ 131 | (frequency: 440.0, 132 | wavelength: 0.7795, 133 | period: 0.00227259 134 | ), 135 | (frequency: 1000.0, 136 | wavelength: 0.343, 137 | period: 0.001 138 | ) 139 | ] 140 | 141 | func indexBounds() { 142 | // Bounds based on min and max frequencies from the config 143 | let minimum = try! WaveCalculator.wavelength(forFrequency: FrequencyValidator.maximumFrequency) 144 | let maximum = try! WaveCalculator.wavelength(forFrequency: FrequencyValidator.minimumFrequency) 145 | let expected = (minimum: minimum, maximum: maximum) 146 | let result = WaveCalculator.wavelengthBounds 147 | 148 | XCTAssertEqual(result.lowerBound, expected.minimum) 149 | XCTAssertEqual(result.upperBound, expected.maximum) 150 | } 151 | 152 | indexBounds() 153 | 154 | func octaveBounds() { 155 | // Bounds based on min and max frequencies from the config 156 | let bounds = WaveCalculator.wavelengthBounds 157 | let minimum = try! WaveCalculator.period(forWavelength: bounds.lowerBound) 158 | let maximum = try! WaveCalculator.period(forWavelength: bounds.upperBound) 159 | let expected = (minimum: minimum, maximum: maximum) 160 | let result = WaveCalculator.periodBounds 161 | 162 | XCTAssertEqual(result.lowerBound, expected.minimum) 163 | XCTAssertEqual(result.upperBound, expected.maximum) 164 | } 165 | 166 | octaveBounds() 167 | 168 | // Valid wavelength 169 | XCTAssertFalse(WaveCalculator.isValid(wavelength: 1_000), "is invalid if value is higher than maximum") 170 | XCTAssertFalse(WaveCalculator.isValid(wavelength: 0.01), "is invalid if value is lower than minimum") 171 | XCTAssertFalse(WaveCalculator.isValid(wavelength: 0), "is invalid if value is zero") 172 | XCTAssertTrue(WaveCalculator.isValid(wavelength: 16), "is valid if value is within valid bounds") 173 | 174 | // Valid period 175 | XCTAssertFalse(WaveCalculator.isValid(period: 10), "is invalid if value is higher than maximum") 176 | XCTAssertFalse(WaveCalculator.isValid(period: 0.0001), "is invalid if value is lower than minimum") 177 | XCTAssertFalse(WaveCalculator.isValid(period: 0), "is invalid if value is zero") 178 | XCTAssertTrue(WaveCalculator.isValid(period: 0.02), "is valid if value is within valid bounds") 179 | 180 | for wave in waves { 181 | let result1 = try! WaveCalculator.frequency(forWavelength: wave.wavelength) 182 | XCTAssertTrue(result1 ≈ (wave.frequency, 0.1)) 183 | let result2 = try! WaveCalculator.wavelength(forFrequency: wave.frequency) 184 | XCTAssertTrue(result2 ≈ (wave.wavelength, 0.1)) 185 | let result3 = try! WaveCalculator.wavelength(forPeriod: wave.period) 186 | XCTAssertTrue(result3 ≈ (wave.wavelength, 0.0001)) 187 | let result4 = try! WaveCalculator.period(forWavelength: wave.wavelength) 188 | XCTAssertTrue(result4 ≈ (wave.period, 0.0001)) 189 | 190 | } 191 | } 192 | 193 | func testAcousticWave() { 194 | let waves = [ 195 | (frequency: 440.0, 196 | wavelength: 0.7795, 197 | period: 0.00227259 198 | ), 199 | (frequency: 1000.0, 200 | wavelength: 0.343, 201 | period: 0.001 202 | ) 203 | ] 204 | 205 | XCTAssertTrue(AcousticWave.speed ≈ (343, 0.001)) 206 | 207 | // Freq init 208 | waves.forEach { 209 | let wave = try! AcousticWave(frequency: $0.frequency) 210 | 211 | XCTAssertTrue(wave.frequency ≈ ($0.frequency, 0.01)) 212 | XCTAssertTrue(wave.wavelength ≈ ($0.wavelength, 0.01)) 213 | XCTAssertTrue(wave.period ≈ ($0.period, 0.01)) 214 | 215 | for (index, value) in wave.harmonics.enumerated() { 216 | XCTAssertTrue(value.frequency ≈ (Double(index + 1) * $0.frequency, 0.01)) 217 | } 218 | } 219 | 220 | // Wave init 221 | waves.forEach { 222 | let wave = try! AcousticWave(wavelength: $0.wavelength) 223 | 224 | XCTAssertTrue(wave.frequency ≈ ($0.frequency, 0.1)) 225 | XCTAssertTrue(wave.wavelength ≈ ($0.wavelength, 0.01)) 226 | XCTAssertTrue(wave.period ≈ ($0.period, 0.01)) 227 | 228 | for (index, value) in wave.harmonics.enumerated() { 229 | XCTAssertTrue(value.frequency ≈ (Double(index + 1) * $0.frequency, 1)) 230 | } 231 | } 232 | 233 | // Period init 234 | waves.forEach { 235 | let wave = try! AcousticWave(period: $0.period) 236 | 237 | XCTAssertTrue(wave.frequency ≈ ($0.frequency, 0.1)) 238 | XCTAssertTrue(wave.wavelength ≈ ($0.wavelength, 0.01)) 239 | XCTAssertTrue(wave.period ≈ ($0.period, 0.01)) 240 | 241 | for (index, value) in wave.harmonics.enumerated() { 242 | XCTAssertTrue(value.frequency ≈ (Double(index + 1) * $0.frequency, 1)) 243 | } 244 | } 245 | } 246 | 247 | func testNotes() { 248 | let letters = Note.Letter.allCases 249 | XCTAssertEqual(letters.count, 12) 250 | 251 | XCTAssertEqual(letters[0], Note.Letter.C) 252 | XCTAssertEqual(letters[1], Note.Letter.CSharp) 253 | XCTAssertEqual(letters[2], Note.Letter.D) 254 | XCTAssertEqual(letters[3], Note.Letter.DSharp) 255 | XCTAssertEqual(letters[4], Note.Letter.E) 256 | XCTAssertEqual(letters[5], Note.Letter.F) 257 | XCTAssertEqual(letters[6], Note.Letter.FSharp) 258 | XCTAssertEqual(letters[7], Note.Letter.G) 259 | XCTAssertEqual(letters[8], Note.Letter.GSharp) 260 | XCTAssertEqual(letters[9], Note.Letter.A) 261 | XCTAssertEqual(letters[10], Note.Letter.ASharp) 262 | XCTAssertEqual(letters[11], Note.Letter.B) 263 | 264 | var note: Note! 265 | 266 | let notes = [ 267 | (index: -9, letter: Note.Letter.C, octave: 4, frequency: 261.626, 268 | string: "C4", lower: "B3", higher: "C#4"), 269 | (index: 16, letter: Note.Letter.CSharp, octave: 6, frequency: 1108.73, 270 | string: "C#6", lower: "C6", higher: "D6"), 271 | (index: 5, letter: Note.Letter.D, octave: 5, frequency: 587.330, 272 | string: "D5", lower: "C#5", higher: "D#5"), 273 | (index: 18, letter: Note.Letter.DSharp, octave: 6, frequency: 1244.51, 274 | string: "D#6", lower: "D6", higher: "E6"), 275 | (index: 31, letter: Note.Letter.E, octave: 7, frequency: 2637.02, 276 | string: "E7", lower: "D#7", higher: "F7"), 277 | (index: -16, letter: Note.Letter.F, octave: 3, frequency: 174.614, 278 | string: "F3", lower: "E3", higher: "F#3"), 279 | (index: -27, letter: Note.Letter.FSharp, octave: 2, frequency: 92.4986, 280 | string: "F#2", lower: "F2", higher: "G2"), 281 | (index: -38, letter: Note.Letter.G, octave: 1, frequency: 48.9994, 282 | string: "G1", lower: "F#1", higher: "G#1"), 283 | (index: -13, letter: Note.Letter.GSharp, octave: 3, frequency: 207.652, 284 | string: "G#3", lower: "G3", higher: "A3"), 285 | (index: 0, letter: Note.Letter.A, octave: 4, frequency: 440, 286 | string: "A4", lower: "G#4", higher: "A#4"), 287 | (index: -47, letter: Note.Letter.ASharp, octave: 0, frequency: 29.1352, 288 | string: "A#0", lower: "A0", higher: "B0"), 289 | (index: 2, letter: Note.Letter.B, octave: 4, frequency: 493.883, 290 | string: "B4", lower: "A#4", higher: "C5") 291 | ] 292 | 293 | // Index 294 | notes.forEach { 295 | note = try! Note(index: $0.index) 296 | XCTAssertNotNil(note) 297 | 298 | XCTAssertEqual(note.index, $0.index) 299 | XCTAssertEqual(note.letter, $0.letter) 300 | XCTAssertEqual(note.octave, $0.octave) 301 | XCTAssertTrue(note.frequency ≈ ($0.frequency, 0.01)) 302 | XCTAssertTrue(note.wave.frequency ≈ ($0.frequency, 0.01)) 303 | XCTAssertEqual(note.description, $0.string) 304 | XCTAssertEqual(try! note.lower().description, $0.lower) 305 | XCTAssertEqual(try! note.higher().description, $0.higher) 306 | } 307 | 308 | // Frequency 309 | notes.forEach { 310 | note = try! Note(frequency: $0.frequency) 311 | XCTAssertNotNil(note) 312 | 313 | XCTAssertEqual(note.index, $0.index) 314 | XCTAssertEqual(note.letter, $0.letter) 315 | XCTAssertEqual(note.octave, $0.octave) 316 | XCTAssertTrue(note.frequency ≈ ($0.frequency, 0.01)) 317 | XCTAssertTrue(note.wave.frequency ≈ ($0.frequency, 0.01)) 318 | XCTAssertEqual(note.description, $0.string) 319 | XCTAssertEqual(try! note.lower().description, $0.lower) 320 | XCTAssertEqual(try! note.higher().description, $0.higher) 321 | } 322 | 323 | // Letter & Octave 324 | notes.forEach { 325 | note = try! Note(letter: $0.letter, octave: $0.octave) 326 | XCTAssertNotNil(note) 327 | 328 | XCTAssertEqual(note.index, $0.index) 329 | XCTAssertEqual(note.letter, $0.letter) 330 | XCTAssertEqual(note.octave, $0.octave) 331 | XCTAssertTrue(note.frequency ≈ ($0.frequency, 0.01)) 332 | XCTAssertTrue(note.wave.frequency ≈ ($0.frequency, 0.01)) 333 | XCTAssertEqual(note.description, $0.string) 334 | XCTAssertEqual(try! note.lower().description, $0.lower) 335 | XCTAssertEqual(try! note.higher().description, $0.higher) 336 | } 337 | } 338 | 339 | func testPitch() { 340 | let offsets = [ 341 | (frequency: 445.0, 342 | lower: Pitch.Offset(note: try! Note(index: 0), frequency: 5, percentage: 19.1, cents: 19.56), 343 | higher: Pitch.Offset(note: try! Note(index: 1), frequency: -21.164, percentage: -80.9, cents: -80.4338), 344 | closest: "A4" 345 | ), 346 | (frequency: 108.0, 347 | lower: Pitch.Offset(note: try! Note(index: -25), frequency: 4.174, percentage: 67.6, cents: 68.2333), 348 | higher: Pitch.Offset(note: try! Note(index: -24), frequency: -2, percentage: -32.39, cents: -31.76), 349 | closest: "A2" 350 | ) 351 | ] 352 | 353 | func offsetInit() { 354 | // Rearrange offsets based on frequency 355 | let sample = offsets[0] 356 | let offsets = Pitch.Offsets(sample.higher, sample.lower) 357 | 358 | XCTAssertEqual(offsets.lower.note.index, sample.lower.note.index) 359 | XCTAssertEqual(offsets.higher.note.index, sample.higher.note.index) 360 | } 361 | 362 | offsetInit() 363 | 364 | offsets.forEach { 365 | let pitch = try! Pitch(frequency: $0.frequency) 366 | 367 | XCTAssertTrue(pitch.frequency ≈ ($0.frequency, 0.01)) 368 | XCTAssertTrue(pitch.wave.frequency ≈ ($0.frequency, 0.01)) 369 | } 370 | 371 | offsets.forEach { 372 | let pitch = try! Pitch(frequency: $0.frequency) 373 | let result = pitch.offsets 374 | 375 | XCTAssertTrue(result.lower.frequency ≈ ($0.lower.frequency, 0.01)) 376 | XCTAssertTrue(result.lower.percentage ≈ ($0.lower.percentage, 0.1)) 377 | XCTAssertEqual(result.lower.note.index, $0.lower.note.index) 378 | XCTAssertTrue(result.lower.cents ≈ ($0.lower.cents, 0.01)) 379 | 380 | XCTAssertTrue(result.higher.frequency ≈ ($0.higher.frequency, 0.01)) 381 | XCTAssertTrue(result.higher.percentage ≈ ($0.higher.percentage, 0.1)) 382 | XCTAssertEqual(result.higher.note.index, $0.higher.note.index) 383 | XCTAssertTrue(result.higher.cents ≈ ($0.higher.cents, 0.01)) 384 | 385 | XCTAssertEqual(result.closest.note.description, $0.closest) 386 | } 387 | } 388 | 389 | static var allTests = [ 390 | ("testFrequencyValidator", testFrequencyValidator), 391 | ("testNoteCalculator", testNoteCalculator), 392 | ("testPichCalculator", testPichCalculator), 393 | ("testWaveCalculator", testWaveCalculator), 394 | ("testAcousticWave", testAcousticWave), 395 | ("testNotes", testNotes), 396 | ("testPitch", testPitch), 397 | ] 398 | } 399 | --------------------------------------------------------------------------------