Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2023 Audio & VideoApp Services

WWDC23 · 22 min · Audio & Video / App Services

Create a great ShazamKit experience

Discover how your app can offer a great audio matching experience with the latest updates to ShazamKit. We’ll take you through matching features, updates to audio recognition, and interactions with the Shazam library. Learn tips and best practices for using ShazamKit in your audio apps. For more on ShazamKit, check out "Create custom catalogs at scale with ShazamKit" from WWDC22 as well as "Explore ShazamKit" and "Create custom audio experiences with ShazamKit" from WWDC21.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 13 snippets

Single match with SHManagedSession swift · at 6:46 ↗
let managedSession = SHManagedSession()

let result = await managedSession.result()
      
switch result {
 case .match(let match):
        print("Match found. MediaItemsCount: \(match.mediaItems.count)")
 case .noMatch(_):
        print("No match found")
 case .error(_, _):
        print("An error occurred")
}
Multiple matches with SHManagedSession swift · at 7:16 ↗
let managedSession = SHManagedSession()

// Continuously match 
for await result in managedSession.results {
          
 switch result {
  case .match(let match):
        print("Match found. MediaItemsCount: \(match.mediaItems.count)")
  case .noMatch(_):
        print("No match found")
  case .error(_, _):
        print("An error occurred")
 }
}
Stop SHManagedSession swift · at 7:37 ↗
let managedSession = SHManagedSession()

// Cancel the session
managedSession.cancel()
ShazamKit Matcher with SHManagedSession swift · at 8:02 ↗
import Foundation
import ShazamKit

struct MatchResult: Identifiable, Equatable {
    let id = UUID()
    let match: SHMatch?
}

@MainActor final class Matcher: ObservableObject {
    
    @Published var isMatching = false
    @Published var currentMatchResult: MatchResult?
    
    var currentMediaItem: SHMatchedMediaItem? {
        currentMatchResult?.match?.mediaItems.first
    }

    private let session: SHManagedSession
    
    init() {
        
        if let catalog = try? ResourcesProvider.catalog() {
            session = SHManagedSession(catalog: catalog)
        } else {
            session = SHManagedSession()
        }
    }
    
    func match() async {
        
        isMatching = true
        
        for await result in session.results {
            switch result {
            case .match(let match):
                Task { @MainActor in
                    self.currentMatchResult = MatchResult(match: match)
                }
            case .noMatch(_):
                print("No match")
                endSession()
            case .error(let error, _):
                print("Error \(error.localizedDescription)")
                endSession()
            }
            stopRecording()
        }
    }
    
    func stopRecording() {
        
        session.cancel()
    }
    
    func endSession() {
        
        // Reset result of any previous match.
        isMatching = false
        currentMatchResult = MatchResult(match: nil)
    }
}
Preparing SHManagedSession swift · at 10:07 ↗
let managedSession = SHManagedSession()

await managedSession.prepare()

let result = await managedSession.result()
SHManagedSession Idle State in SwiftUI swift · at 11:39 ↗
struct MatchView: View {
    let session: SHManagedSession
    
    var body: some View {
        VStack {
             Text(session.state == .idle ? "Hear Music?" 
                : "Matching")
             if session.state == .matching {
                  ProgressView()
             } else {
                  Button {
                      // start match
                  } label: {
                      Text("Learn the Dance")
                  }
             }
        }       
}
SHManagedSession Matching State in SwiftUI swift · at 12:25 ↗
struct MatchView: View {
    let session: SHManagedSession
    
    var body: some View {
        VStack {
             Text(session.state == .idle ? "Hear Music?" 
                : "Matching")
             if session.state == .matching {
                  ProgressView()
             } else {
                  Button {
                      // start match
                  } label: {
                      Text("Learn the Dance")
                  }
             }
        }    
    }
}
Adding with SHLibrary swift · at 15:23 ↗
func add(mediaItems: [SHMediaItem]) async throws {

    try await SHLibrary.default.addItems(mediaItems)
}
Reading with SHLibrary swift · at 15:34 ↗
struct LibraryView: View {
    var body: some View {
        List(SHLibrary.default.items) { item in
            MediaItemView(item: item)
        }
    }
}
Reading with SHLibrary in a non-UI context swift · at 16:00 ↗
// Determine a user’s most popular genre
   
let currentItems = await SHLibrary.default.items

let genres = currentItems.flatMap { $0.genres }

// count frequency of genres and get the highest
let mostPopularGenre = highestOccurringGenre(from: genres)
SHLibrary Remove swift · at 16:25 ↗
func remove(mediaItems: [SHMediaItem]) async throws {
    
    try await SHLibrary.default.removeItems(mediaItems)
}
RecentDancesView with SHLibrary read and delete implementation swift · at 16:42 ↗
import SwiftUI
import ShazamKit

enum NavigationPath: Hashable {
    case nowPlayingView(videoURL: URL)
    case danceCompletionView
}

struct RecentDancesView: View {
    private enum ViewConstants {
        static let emptyStateImageName: String = "EmptyStateIcon"
        static let emptyStateTextTitle: String = "No Dances Yet?"
        static let emptyStateTextSubtitle: String = "Find some music to start learning"
        static let deleteSwipeViewOpacity: Double = 0.5
        static let matchingStateTextTopPadding: CGFloat = 24
        static let matchingStateTextBottomPadding: CGFloat = 16
        static let progressViewScaleEffect: CGFloat = 1.1
        static let progressViewBottomPadding: CGFloat = 12.0
        static let learnDanceButtonWidth: CGFloat = 250
        static let curvedTopSideRectangleHeight: CGFloat = 200
        static let listRowBottomInset: CGFloat = 30.0
        static let matchingStateText: String = "Get Ready..."
        static let notMatchingStateText: String = "Hear Music?"
        static let noMatchText: String = "No dance video for audio"
        static let navigationTitleText: String = "Recent Dances"
        static let learnDanceButtonText: String = "Learn the Dance"
        static let retryButtonText: String = "Try Again"
        static let cancelButtonText: String = "Cancel"
    }
    
    // MARK: Properties
    private var isListEmpty: Bool {
        SHLibrary.default.items.isEmpty
    }
    
    @State private var matchingState: String = ViewConstants.notMatchingStateText
    @State private var matchButtonText: String = ViewConstants.learnDanceButtonText
    @State private var canRetryMatchAttempt = false
    @State private var navigationPath: [NavigationPath] = []
    
    // MARK: Environment
    @EnvironmentObject private var matcher: Matcher
    @Environment(\.openURL) var openURL
    var body: some View {
        NavigationStack(path: $navigationPath) {
            ZStack(alignment: .bottom) {
                List(SHLibrary.default.items, id: \.self) { mediaItem in
                        RecentDanceRowView(mediaItem: mediaItem)
                            .onTapGesture(perform: {
                                guard let appleMusicURL = mediaItem.appleMusicURL else {
                                    return
                                }
                                openURL(appleMusicURL)
                            })
                            .swipeActions {
                                Button {
                                    Task { try? await SHLibrary.default.removeItems([mediaItem]) }
                                } label: {
                                    Image(systemName: "trash")
                                        .symbolRenderingMode(.hierarchical)
                                }
                                .tint(.appPrimary.opacity(0.5))
                            }
                }
                    .listStyle(.plain)
                    .overlay {
                        if isListEmpty {
                            ContentUnavailableView {
                                Label(ViewConstants.emptyStateTextTitle,
                                      image: ImageResource(name: ViewConstants.emptyStateImageName, bundle: Bundle.main))
                                    .font(.title)
                                    .foregroundStyle(Color.white)
                            } description: {
                                Text(ViewConstants.emptyStateTextSubtitle)
                                    .foregroundStyle(Color.white)
                            }
                        }
                    }
                    .safeAreaInset(edge: .bottom, spacing: ViewConstants.listRowBottomInset) {
                        ZStack(alignment: .top) {
                            CurvedTopSideRectangle()
                            VStack {
                                Text(matchingState)
                                    .font(.body)
                                    .foregroundStyle(.white)
                                    .padding(.top, ViewConstants.matchingStateTextTopPadding)
                                    .padding(.bottom, ViewConstants.matchingStateTextBottomPadding)
                                if matcher.isMatching {
                                    ProgressView()
                                        .progressViewStyle(.circular)
                                        .tint(.appPrimary)
                                        .scaleEffect(x: ViewConstants.progressViewScaleEffect, y: ViewConstants.progressViewScaleEffect)
                                        .padding(.bottom, ViewConstants.progressViewBottomPadding)
                                    Button(ViewConstants.cancelButtonText) {
                                        canRetryMatchAttempt = false
                                        matcher.stopRecording()
                                        matcher.endSession()
                                    }
                                    .foregroundStyle(Color.appPrimary)
                                    .font(.subheadline)
                                    .fontWeight(.semibold)
                                } else {
                                    Button {
                                        Task { await matcher.match() }
                                        matchingState = ViewConstants.matchingStateText
                                        canRetryMatchAttempt = true
                                    } label: {
                                        Text(matchButtonText)
                                            .foregroundStyle(.black)
                                            .font(.title3)
                                            .fontWeight(.heavy)
                                            .frame(maxWidth: .infinity)
                                    }
                                    .frame(width: ViewConstants.learnDanceButtonWidth)
                                    .padding()
                                    .background(Color.appPrimary)
                                    .clipShape(Capsule())
                                }
                            }
                        }
                        .edgesIgnoringSafeArea(.bottom)
                        .frame(height: ViewConstants.curvedTopSideRectangleHeight)
                    }
            }
            .background(Color.appSecondary)
            .navigationTitle(isListEmpty ? "" : ViewConstants.navigationTitleText)
            .preferredColorScheme(.dark)
            .toolbarColorScheme(.dark, for: .navigationBar)
            .navigationBarTitleDisplayMode(.large)
            .toolbarBackground(Color.appSecondary, for: .navigationBar)
            .frame(maxHeight: .infinity)
            .onChange(of: matcher.currentMatchResult, { _, result in
                
                guard navigationPath.isEmpty else {
                    print("Dance video already displayed")
                    return
                }
                
                guard let match = result?.match,
                      let url = ResourcesProvider.videoURL(forFilename: match.mediaItems.first?.videoTitle ?? "") else {
                    
                    matchingState = canRetryMatchAttempt ? ViewConstants.noMatchText : ViewConstants.notMatchingStateText
                    matchButtonText = canRetryMatchAttempt ? ViewConstants.retryButtonText : ViewConstants.learnDanceButtonText
                    return
                }
                
                canRetryMatchAttempt = false
                
                // Add the video playing view to the navigation stack.
                navigationPath.append(.nowPlayingView(videoURL: url))
            })
            .navigationDestination(for: NavigationPath.self, destination: { newNavigationPath in
                switch newNavigationPath {
                case .nowPlayingView(let videoURL):
                    NowPlayingView(navigationPath: $navigationPath, nowPlayingViewModel: NowPlayingViewModel(player: AVPlayer(url: videoURL)))
                case .danceCompletionView:
                    DanceCompletionView(navigationPath: $navigationPath)
                }
            })
            .onAppear {
                if AVAudioSession.sharedInstance().category != .ambient {
                    Task.detached { try? AVAudioSession.sharedInstance().setCategory(.ambient) }
                }
                matchingState = ViewConstants.notMatchingStateText
                matchButtonText = ViewConstants.learnDanceButtonText
            }
        }
    }
}
Filtering for specific media items swift · at 20:23 ↗
func match(from televisionShowCatalog: SHCustomCatalog) async -> [SHMatchedMediaItem] {
        
        let managedSession = SHManagedSession(catalog: televisionShowCatalog)
        
        let result = await managedSession.result()
        
        if case .match(let match) = result {
            
            // filter for only media items related to a particular episode
            let filteredMediaItems = match.mediaItems.filter { $0.title == "Episode 2" }
            return filteredMediaItems
        }
        
        return []
}

Resources