├── 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 | 
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 |
--------------------------------------------------------------------------------