Skip to content

Commit

Permalink
Completely rework similarity checks
Browse files Browse the repository at this point in the history
  • Loading branch information
Philipp Wallisch committed Jun 18, 2021
1 parent a80be4d commit db2b228
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 40 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ let duration = testFingerprint.duration // 46.0
Get the fingerprint as base64 representation

``` swift
let base64FingerprintString = testFingerprint.fingerprint! // AQABYJGikFSmJBCPijt6Hq..."
let base64FingerprintString = testFingerprint.fingerprint // "AQABYJGikFSmJBCPijt6Hq..."
```

Get the fingerprints hash as binary string

``` swift
let binaryHashString = testFingerprint.hash! // "01110100010011101010100110100100"
let binaryHashString = testFingerprint.hash // "01110100010011101010100110100100"
```

Instantiate a fingerprint object from its base64 representation and entire file duration
Expand All @@ -74,13 +74,23 @@ let newFingerprint = try AudioFingerprint(from: base64FingerprintString, duratio
Get similarity to other fingerprint object (`0.0` to `1.0`)

``` swift
newFingerprint.similarity(to: testFingerprint) // 1.0
let similarity = try newFingerprint.similarity(to: testFingerprint) // 1.0
```

Or a hash as binary string
Optionally, ignore length differences greater than 50% between fingerprints (Default: `false`)

*Note: This can lead to wrong results when comparing e.g. a Fingerprint sampled for 10 seconds to a Fingerprint sampled for 120 seconds*

``` swift
let similarity = try newFingerprint.similarity(to: testFingerprint, ignoreLength: true) // 1.0
```

You can also get the similarity to a fingerprint hash

*Note: This is less accurate than comparing fingerprint objects, especially if the algorithms don't match*

``` swift
newFingerprint.similarity(to: binaryHashString) // 1.0
let hashSimilarity = try newFingerprint.similarity(to: binaryHashString) // 1.0
```

### Looking up fingerprints
Expand Down Expand Up @@ -142,7 +152,7 @@ Or an `AudioFingerprint.Error`

``` swift
do {
try AudioFingerprint(from: "Invalid", duration: 10.0)
try AudioFingerprint(from: "Invalid", duration: 46.0)
} catch {
// AudioFingerprint.Error.invalidFingerprint
}
Expand Down
7 changes: 1 addition & 6 deletions Sources/ChromaSwift/AcoustID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,11 @@ public class AcoustID {
}

public func lookup(_ fingerprint: AudioFingerprint, completion: @escaping (Result<[APIResult], Error>) -> Void) {
guard let base64Fingerprint = fingerprint.fingerprint else {
completion(.failure(Error.invalidFingerprint))
return
}

let query = [
URLQueryItem(name: "client", value: apiKey),
URLQueryItem(name: "meta", value: "recordings+releasegroups+compress"),
URLQueryItem(name: "duration", value: String(UInt(fingerprint.duration))),
URLQueryItem(name: "fingerprint", value: base64Fingerprint)
URLQueryItem(name: "fingerprint", value: fingerprint.fingerprint)
]
var lookupURLComponents = URLComponents(string: lookupEndpoint)!
lookupURLComponents.queryItems = query
Expand Down
59 changes: 38 additions & 21 deletions Sources/ChromaSwift/AudioFingerprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class AudioFingerprint {
case fingerprintingFailed
case invalidDuration
case invalidFingerprint
case invalidHash
case differentAlgorithm
case lenghtDifference
}

public enum Algorithm: Int32 {
Expand Down Expand Up @@ -78,33 +81,27 @@ public class AudioFingerprint {
self.init(from: [UInt32](UnsafeBufferPointer(start: rawFingerprintPointer, count: Int(rawFingerprintSize))), algorithm: Algorithm(rawValue: algorithm)!, duration: duration)
}

public var fingerprint: String? {
public var fingerprint: String {
var fingerprint: UnsafeMutablePointer<CChar>? = UnsafeMutablePointer<CChar>.allocate(capacity: 1)
defer {
fingerprint?.deallocate()
}
var fingerprintSize = Int32(0)

if chromaprint_encode_fingerprint(&rawFingerprint, Int32(rawFingerprint.count), algorithm.rawValue, &fingerprint, &fingerprintSize, 1) != 1 {
return nil
}
chromaprint_encode_fingerprint(&rawFingerprint, Int32(rawFingerprint.count), algorithm.rawValue, &fingerprint, &fingerprintSize, 1)

return String(cString: fingerprint!)
}

var rawHash: UInt32? {
var rawHash: UInt32 {
var hash = UInt32(0)

if chromaprint_hash_fingerprint(&rawFingerprint, Int32(rawFingerprint.count), &hash) != 1 {
return nil
}
chromaprint_hash_fingerprint(&rawFingerprint, Int32(rawFingerprint.count), &hash)

return hash
}

public var hash: String? {
guard let rawHash = rawHash else { return nil }

public var hash: String {
let binaryString = String(rawHash, radix: 2)

var paddedBinaryString = binaryString
Expand All @@ -115,19 +112,39 @@ public class AudioFingerprint {
return paddedBinaryString
}

func similarity(to otherRawHash: UInt32) -> Double? {
guard let selfRawHash = rawHash else { return nil }
let diff = selfRawHash ^ otherRawHash
return Double(32 - String(diff, radix: 2).filter({ $0 == "1" }).count) / Double(32)
func similarity(to rawHash: UInt32) -> Double {
return Double(32 - (self.rawHash ^ rawHash).nonzeroBitCount) / Double(32)
}

public func similarity(to otherHash: String) -> Double? {
guard let otherRawHash = UInt32(otherHash, radix: 2) else { return nil }
return similarity(to: otherRawHash)
public func similarity(to hash: String) throws -> Double {
guard let rawHash = UInt32(hash, radix: 2) else {
throw Error.invalidHash
}
return similarity(to: rawHash)
}

public func similarity(to fingerprint: AudioFingerprint) -> Double? {
guard let otherRawHash = fingerprint.rawHash else { return nil }
return similarity(to: otherRawHash)
public func similarity(to fingerprint: AudioFingerprint, ignoreLength: Bool = false) throws -> Double {
if fingerprint.algorithm != algorithm {
throw Error.differentAlgorithm
}

let sampleDifference = fingerprint.rawFingerprint.count - rawFingerprint.count
let biggerFingerprint = sampleDifference.signum() >= 0 ? fingerprint.rawFingerprint : rawFingerprint
let smallerFingerprint = sampleDifference.signum() >= 0 ? rawFingerprint : fingerprint.rawFingerprint

if !ignoreLength && abs(sampleDifference) > biggerFingerprint.count / 2 {
throw Error.lenghtDifference
}

var smallestError = Int.max
for offset in 0...abs(sampleDifference) {
var error = 0
for (index, value) in smallerFingerprint.enumerated() {
error += (value ^ biggerFingerprint[index + offset]).nonzeroBitCount
}
smallestError = error < smallestError ? error : smallestError
}

return 1 - Double(smallestError) / 32 / Double(smallerFingerprint.count)
}
}
39 changes: 32 additions & 7 deletions Tests/ChromaSwiftTests/ChromaSwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class ChromaSwiftTests: XCTestCase {
let result = try AudioFingerprint(from: backbeatURL, algorithm: .test4)
XCTAssertEqual(result.algorithm, AudioFingerprint.Algorithm.test4)

let constructedResult = try AudioFingerprint(from: result.fingerprint!, duration: result.duration)
let constructedResult = try AudioFingerprint(from: result.fingerprint, duration: result.duration)
XCTAssertEqual(constructedResult.algorithm, AudioFingerprint.Algorithm.test4)
}

Expand All @@ -89,31 +89,56 @@ class ChromaSwiftTests: XCTestCase {

let longResult = try AudioFingerprint(from: fireworksURL, maxSampleDuration: nil)
XCTAssertEqual(UInt(longResult.duration), 191)
XCTAssertGreaterThan(longResult.fingerprint!.count, result.fingerprint!.count)
XCTAssertGreaterThan(longResult.fingerprint.count, result.fingerprint.count)

let shortResult = try AudioFingerprint(from: fireworksURL, maxSampleDuration: 10.0)
XCTAssertEqual(UInt(shortResult.duration), 191)
XCTAssertLessThan(shortResult.fingerprint!.count, result.fingerprint!.count)
XCTAssertLessThan(shortResult.fingerprint.count, result.fingerprint.count)
}

func testSimilarity() throws {
let result = try AudioFingerprint(from: backbeatURL)
let compareResult = try AudioFingerprint(from: backbeatFingerprint, duration: 46.0)
XCTAssertEqual(try result.similarity(to: result), 1.00)
XCTAssertGreaterThan(try result.similarity(to: compareResult), 0.99)

let otherResult = try AudioFingerprint(from: fireworksURL)
let otherCompareResult = try AudioFingerprint(from: fireworksFingerprint, duration: 191.0)
XCTAssertEqual(try otherResult.similarity(to: otherResult), 1.00)
XCTAssertGreaterThan(try otherResult.similarity(to: otherCompareResult), 0.99)

XCTAssertEqual(result.similarity(to: otherResult), 0.78125)
XCTAssertLessThan(try result.similarity(to: otherResult, ignoreLength: true), 0.55)
}

func testInvalidSimilarity() throws {
let result = try AudioFingerprint(from: backbeatFingerprint, duration: 46.0)
let resultTest4 = try AudioFingerprint(from: backbeatFingerprintTest4, duration: 46.0)
let otherResult = try AudioFingerprint(from: fireworksFingerprint, duration: 191.0)

XCTAssertThrowsError(try result.similarity(to: otherResult)) { error in
XCTAssertEqual(error as? AudioFingerprint.Error, AudioFingerprint.Error.lenghtDifference)
}

XCTAssertEqual(resultTest4.algorithm, AudioFingerprint.Algorithm.test4)
XCTAssertThrowsError(try result.similarity(to: resultTest4)) { error in
XCTAssertEqual(error as? AudioFingerprint.Error, AudioFingerprint.Error.differentAlgorithm)
}
}

func testHashSimilarity() throws {
let result = try AudioFingerprint(from: backbeatURL)

XCTAssertEqual(result.similarity(to: backbeatHash), 1.0)
XCTAssertEqual(try result.similarity(to: backbeatHash), 1.0)

XCTAssertEqual(try result.similarity(to: fireworksHash), 0.78125)
}

func testInvalidHashSimilarity() throws {
let result = try AudioFingerprint(from: backbeatURL)
let result = try AudioFingerprint(from: backbeatFingerprint, duration: 46.0)

XCTAssertNil(result.similarity(to: "Invalid"))
XCTAssertThrowsError(try result.similarity(to: "Invalid")) { error in
XCTAssertEqual(error as? AudioFingerprint.Error, AudioFingerprint.Error.invalidHash)
}
}

func testAcoustIDInvalidAPIKey() throws {
Expand Down

0 comments on commit db2b228

Please sign in to comment.