🌐 KO

🎀 ShazamKit

⭐ Difficulty: ⭐⭐ ⏱️ Est. Time: 1-2h πŸ“‚ Graphics & Media

μ˜€λ””μ˜€ 인식 및 μŒμ•… λ§€μΉ­ ν”„λ ˆμž„μ›Œν¬

iOS 15+μ˜€λ””μ˜€ 인식

✨ ShazamKit is?

ShazamKit is a framework that integrates Shazam's audio recognition technology into your app. It recognizes playing music in real-time, creates custom audio catalogs for in-app content matching, and accesses Shazam's vast music database. Useful for delivering audio-based experiences in music apps, games, educational apps and more.

πŸ’‘ Key Features: Music Recognition Β· Custom Catalogs Β· Real-Time Matching Β· Shazam Library Access Β· Audio Signature Generation Β· Offline Recognition

🎡 1. κΈ°λ³Έ μŒμ•… 인식

Use SHSession to recognize music from the Shazam catalog.

ShazamManager.swift β€” Basic Music Recognition
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. μ˜€λ””μ˜€ 파일 인식

μ €μž₯된 μ˜€λ””μ˜€ νŒŒμΌμ—μ„œ μŒμ•…μ„ 인식.

AudioFileRecognition.swift β€” File Recognition
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. μ»€μŠ€ν…€ μΉ΄νƒˆλ‘œκ·Έ

Create your own audio catalog to recognize in-app content.

CustomCatalog.swift β€” Custom Catalog
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. μ‹œκ·Έλ‹ˆμ²˜ μ €μž₯ 및 λ‘œλ“œ

Save generated signatures to file for later reuse.

SignatureStorage.swift β€” Signature Management
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)")
    }
}

πŸ“± Complete Example

ShazamAppView.swift β€” Shazam App
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 Guidelines

🎯 Practical Usage

πŸ“š Learn More

⚑️ Performance Tips: Custom catalogs can be bundled with the app or downloaded from a server. Signature generation is CPU-intensive, so run it on a background thread. The Shazam catalog requires network, but custom catalogs work offline.

πŸ“Ž Apple Official Resources

πŸ“˜ Documentation 🎬 WWDC Sessions