π€ ShazamKit
μ€λμ€ μΈμ λ° μμ λ§€μΉ νλ μμν¬
β¨ ShazamKitμ΄λ?
ShazamKitμ Shazamμ μ€λμ€ μΈμ κΈ°μ μ μ±μ ν΅ν©ν μ μλ νλ μμν¬μ λλ€. μ¬μ μ€μΈ μμ μ μ€μκ°μΌλ‘ μΈμνκ³ , 컀μ€ν μ€λμ€ μΉ΄νλ‘κ·Έλ₯Ό λ§λ€μ΄ μ± λ΄ μ€λμ€ μ½ν μΈ λ₯Ό λ§€μΉνλ©°, Shazamμ λ°©λν μμ λ°μ΄ν°λ² μ΄μ€μ μ κ·Όν μ μμ΅λλ€. μμ μ±, κ²μ, κ΅μ‘ μ± λ±μμ μ€λμ€ κΈ°λ° κ²½νμ μ 곡ν λ μ μ©ν©λλ€.
π΅ 1. κΈ°λ³Έ μμ μΈμ
SHSessionμ μ¬μ©νμ¬ Shazam μΉ΄νλ‘κ·Έμμ μμ μ μΈμν©λλ€.
import ShazamKit import AVFoundation import SwiftUI @Observable class ShazamManager: NSObject, SHSessionDelegate { private var session: SHSession? private var audioEngine: AVAudioEngine? var matchedMedia: SHMatchedMediaItem? var isListening = false var errorMessage: String? override init() { super.init() setupSession() } // μΈμ μ€μ private func setupSession() { // Shazam μΉ΄νλ‘κ·Έ μΈμ μμ± session = SHSession() session?.delegate = self print("β Shazam μΈμ μμ±λ¨") } // μμ μΈμ μμ func startListening() { guard !isListening else { return } do { // μ€λμ€ μΈμ μ€μ let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.record) try audioSession.setActive(true) // μ€λμ€ μμ§ μ€μ audioEngine = AVAudioEngine() guard let audioEngine = audioEngine else { return } let inputNode = audioEngine.inputNode let bus = 0 let bufferSize: AVAudioFrameCount = 2048 // μ€λμ€ ν μ€μΉ inputNode.installTap(onBus: bus, bufferSize: bufferSize, format: inputNode.outputFormat(forBus: bus)) { [weak self] buffer, time in self?.session?.matchStreamingBuffer(buffer, at: time) } // μ€λμ€ μμ§ μμ try audioEngine.start() isListening = true errorMessage = nil print("π€ μμ μΈμ μμ") } catch { errorMessage = "μ€λμ€ μμ§ μμ μ€ν¨: \(error.localizedDescription)" print("β \(errorMessage ?? "")") } } // μμ μΈμ μ€μ§ func stopListening() { guard isListening else { return } audioEngine?.stop() audioEngine?.inputNode.removeTap(onBus: 0) audioEngine = nil isListening = false print("βΉοΈ μμ μΈμ μ€μ§") } // MARK: - SHSessionDelegate func session(_ session: SHSession, didFind match: SHMatch) { guard let mediaItem = match.mediaItems.first else { return } Task { @MainActor in matchedMedia = mediaItem print("π΅ λ°κ²¬: \(mediaItem.title ?? "Unknown") - \(mediaItem.artist ?? "Unknown")") } } func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) { Task { @MainActor in errorMessage = "μμ μ μ°Ύμ μ μμ΅λλ€" print("β λ§€μΉ μ€ν¨") } } }
π¦ 2. μ€λμ€ νμΌ μΈμ
μ μ₯λ μ€λμ€ νμΌμμ μμ μ μΈμν©λλ€.
import ShazamKit import AVFoundation extension ShazamManager { // μ€λμ€ νμΌμμ μκ·Έλμ² μμ± func generateSignature(from url: URL) async throws -> SHSignature { // μ€λμ€ νμΌ μ½κΈ° let file = try AVAudioFile(forReading: url) // μκ·Έλμ² μμ±κΈ° let generator = SHSignatureGenerator() // λ²νΌ μ½κΈ° λ° μκ·Έλμ² μμ± let bufferSize: AVAudioFrameCount = 2048 guard let buffer = AVAudioPCMBuffer( pcmFormat: file.processingFormat, frameCapacity: bufferSize ) else { throw NSError(domain: "AudioError", code: -1) } while file.framePosition < file.length { try file.read(into: buffer) try generator.append(buffer, at: nil) } // μκ·Έλμ² λ°ν return generator.signature() } // νμΌμμ μμ μΈμ func recognizeAudioFile(at url: URL) async { do { // μκ·Έλμ² μμ± let signature = try await generateSignature(from: url) // Shazam μΉ΄νλ‘κ·Έμμ λ§€μΉ let match = try await session?.match(signature) if let mediaItem = match?.mediaItems.first { matchedMedia = mediaItem print("π΅ νμΌ μΈμ μ±κ³΅: \(mediaItem.title ?? "Unknown")") } } catch { errorMessage = "νμΌ μΈμ μ€ν¨: \(error.localizedDescription)" print("β \(errorMessage ?? "")") } } }
ποΈ 3. 컀μ€ν μΉ΄νλ‘κ·Έ
μ체 μ€λμ€ μΉ΄νλ‘κ·Έλ₯Ό λ§λ€μ΄ μ± λ΄ μ½ν μΈ λ₯Ό μΈμν©λλ€.
import ShazamKit import AVFoundation @Observable class CustomCatalogManager: NSObject, SHSessionDelegate { private var session: SHSession? private var catalog: SHCustomCatalog? var matchedReference: SHMediaItem? var isListening = false // 컀μ€ν μΉ΄νλ‘κ·Έ μμ± func createCustomCatalog(from audioFiles: [URL]) async throws { var referenceSignatures: [SHSignature] = [] // κ° μ€λμ€ νμΌμμ μκ·Έλμ² μμ± for (index, fileURL) in audioFiles.enumerated() { let file = try AVAudioFile(forReading: fileURL) let generator = SHSignatureGenerator() let bufferSize: AVAudioFrameCount = 2048 guard let buffer = AVAudioPCMBuffer( pcmFormat: file.processingFormat, frameCapacity: bufferSize ) else { continue } while file.framePosition < file.length { try file.read(into: buffer) try generator.append(buffer, at: nil) } let signature = generator.signature() referenceSignatures.append(signature) print("β μκ·Έλμ² μμ±: \(fileURL.lastPathComponent)") } // λ―Έλμ΄ μμ΄ν μμ± var mediaItems: [SHMediaItem] = [] for (index, signature) in referenceSignatures.enumerated() { let mediaItem = SHMediaItem( properties: [ .title: "Track \(index + 1)", .artist: "Custom Artist" ] ) mediaItems.append(mediaItem) } // 컀μ€ν μΉ΄νλ‘κ·Έ μμ± catalog = SHCustomCatalog() for (signature, mediaItem) in zip(referenceSignatures, mediaItems) { try await catalog?.addReferenceSignature(signature, representing: [mediaItem]) } // 컀μ€ν μΉ΄νλ‘κ·Έ μΈμ μμ± if let catalog = catalog { session = SHSession(catalog: catalog) session?.delegate = self print("β 컀μ€ν μΉ΄νλ‘κ·Έ μμ± μλ£") } } // 컀μ€ν μΉ΄νλ‘κ·Έμμ λ§€μΉ func matchAgainstCustomCatalog(signature: SHSignature) async { guard let session = session else { return } do { let match = try await session.match(signature) if let mediaItem = match.mediaItems.first { matchedReference = mediaItem print("π΅ 컀μ€ν μΉ΄νλ‘κ·Έ λ§€μΉ: \(mediaItem.title ?? "Unknown")") } } catch { print("β 컀μ€ν λ§€μΉ μ€ν¨: \(error)") } } // MARK: - SHSessionDelegate func session(_ session: SHSession, didFind match: SHMatch) { guard let mediaItem = match.mediaItems.first else { return } Task { @MainActor in matchedReference = mediaItem print("π΅ 컀μ€ν λ§€μΉ μ±κ³΅") } } func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) { print("β 컀μ€ν λ§€μΉ μ€ν¨") } }
πΎ 4. μκ·Έλμ² μ μ₯ λ° λ‘λ
μμ±ν μκ·Έλμ²λ₯Ό νμΌλ‘ μ μ₯νκ³ λμ€μ λ€μ μ¬μ©ν©λλ€.
import ShazamKit import Foundation class SignatureStorage { // μκ·Έλμ²λ₯Ό νμΌλ‘ μ μ₯ func saveSignature(_ signature: SHSignature, to fileName: String) throws { // μκ·Έλμ² λ°μ΄ν° let data = signature.dataRepresentation // μ μ₯ κ²½λ‘ let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("\(fileName).shazamsignature") // νμΌ μ μ₯ try data.write(to: url) print("β μκ·Έλμ² μ μ₯: \(url)") } // νμΌμμ μκ·Έλμ² λ‘λ func loadSignature(from fileName: String) throws -> SHSignature { // νμΌ κ²½λ‘ let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("\(fileName).shazamsignature") // λ°μ΄ν° μ½κΈ° let data = try Data(contentsOf: url) // μκ·Έλμ² μμ± let signature = try SHSignature(dataRepresentation: data) print("β μκ·Έλμ² λ‘λ: \(url)") return signature } // μ μ₯λ λͺ¨λ μκ·Έλμ² λͺ©λ‘ func listSavedSignatures() -> [String] { let documentsURL = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] do { let files = try FileManager.default .contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) .filter { $0.pathExtension == "shazamsignature" } .map { $0.deletingPathExtension().lastPathComponent } return files } catch { print("β νμΌ λͺ©λ‘ κ°μ Έμ€κΈ° μ€ν¨: \(error)") return [] } } // μκ·Έλμ² μμ func deleteSignature(fileName: String) throws { let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("\(fileName).shazamsignature") try FileManager.default.removeItem(at: url) print("β μκ·Έλμ² μμ : \(fileName)") } }
π± μ’ ν© μμ
import SwiftUI import ShazamKit struct ShazamAppView: View { @State private var shazamManager = ShazamManager() @State private var showingResult = false var body: some View { NavigationStack { VStack(spacing: 30) { // λ²νΌ Button { if shazamManager.isListening { shazamManager.stopListening() } else { shazamManager.startListening() } } label: { ZStack { Circle() .fill(shazamManager.isListening ? Color.red : Color.blue) .frame(width: 150, height: 150) if shazamManager.isListening { Image(systemName: "waveform.circle.fill") .font(.system(size: 60)) .foregroundStyle(.white) } else { Image(systemName: "magnifyingglass") .font(.system(size: 60)) .foregroundStyle(.white) } } } .buttonStyle(.plain) Text(shazamManager.isListening ? "μΈμ μ€..." : "ννμ¬ μμ μΈμ") .font(.headline) // κ²°κ³Ό νμ if let media = shazamManager.matchedMedia { VStack(spacing: 12) { AsyncImage(url: media.artworkURL) { image in image .resizable() .scaledToFit() } placeholder: { Rectangle() .fill(Color.gray.opacity(0.3)) } .frame(width: 200, height: 200) .cornerRadius(12) VStack(spacing: 4) { Text(media.title ?? "Unknown") .font(.title2) .fontWeight(.bold) Text(media.artist ?? "Unknown Artist") .font(.subheadline) .foregroundStyle(.secondary) } if let appleMusicURL = media.appleMusicURL { Link("Apple Musicμμ μ΄κΈ°", destination: appleMusicURL) .buttonStyle(.borderedProminent) } if let webURL = media.webURL { Link("μΉμμ 보기", destination: webURL) .buttonStyle(.bordered) } } .padding() } // μλ¬ λ©μμ§ if let error = shazamManager.errorMessage { Text(error) .foregroundStyle(.red) .font(.caption) } Spacer() } .padding() .navigationTitle("μμ μΈμ") } } }
π‘ HIG κ°μ΄λλΌμΈ
- κΆν: λ§μ΄ν¬ κΆν νμ (Info.plistμ
NSMicrophoneUsageDescriptionμΆκ°) - νΌλλ°±: μΈμ μ€μμ λͺ νν νμ
- κ²°κ³Ό νμ: μ¨λ² μνΈ, μ λͺ©, μν°μ€νΈλ₯Ό μκ°μ μΌλ‘ νμ
- λ§ν¬ μ 곡: Apple Music λλ μΉ λ§ν¬ μ 곡
- μ€νλΌμΈ: μΈν°λ· μ°κ²° νμ (Shazam μΉ΄νλ‘κ·Έ μ¬μ© μ)
π― μ€μ νμ©
- μμ μ±: μ¬μ μ€μΈ μμ μλ μΈμ
- λΌλμ€ μ±: λ°©μ‘ μ€μΈ μμ μλ³
- κ²μ: 컀μ€ν μΉ΄νλ‘κ·Έλ‘ λ°°κ²½ μμ νΈλνΉ
- κ΅μ‘ μ±: μ€λμ€ μ½ν μΈ μ§λ μΆμ
- μ΄λ²€νΈ μ±: κ³΅μ° μ€ κ³‘ μ 보 μ 곡