WWDC Quick Look 💓 By SwiftGGTeam
Create a great ShazamKit experience

Create a great ShazamKit experience

元の動画を見る

ハイライト

ShazamKit に SHManagedSession が追加され、マイク録音とオーディオエンジン設定を自動処理。数行のコードで音声認識が可能になりました。SHLibrary は読み書き削除に対応し、デバイス間同期もサポート。SHSession は PCM フォーマット対応と複数マッチ返却機能が拡張されました。

主要内容

以前: 音声認識にはオーディオエンジンの手動設定が必要

ShazamKit で音声認識を行うには、以前は AVAudioEngine を手動で設定する必要がありました。tap をインストールして PCM バッファを受信し、オーディオエンジンを準備し、録音権限をリクエストし、録音を開始し、バッファを SHSession に渡す—この一連の流れはオーディオプログラミングに不慣れな開発者には扱いにくく、コード量も多くエラーが起きやすいものでした。

現在: SHManagedSession で数行のコード

iOS 17 では SHManagedSession が導入され、録音関連の作業をすべて自動処理します。インスタンスを作成し、result() を呼ぶか results を反復処理し、返された match/noMatch/error を処理するだけです。

SHManagedSession は 2 つのモードをサポートします:

  • 単発認識: result() を呼び出して 1 件の結果を取得
  • 継続認識: results async sequence を反復処理し、長時間のリスニングに適用

cancel() で録音とマッチングを停止します。prepare() でリソースを事前割り当てし、プリレコーディングを開始することで、次の認識リクエストへの応答を高速化できます。

SHLibrary: 読み書き削除を一体化

以前の SHMediaLibrary はマッチ結果の書き込みのみ可能で、有効な Shazam ID を持つエントリのみ書き込めました。新しい SHLibrary は以下をサポートします:

  • addItems(_:) でメディア項目を追加
  • items で現在のアプリが追加したすべてのエントリを読み取り
  • removeItems(_:) でエントリを削除

読み取りと削除は本アプリが追加したコンテンツのみ操作でき、他のアプリでのユーザーの Shazam 記録は公開されません。SHLibraryObservable に準拠し、SwiftUI ビューが自動的に更新されます。

より多くのフォーマット対応と複数マッチ

SHSessionmatchStreamingBuffer は以前、特定のサンプルレートの PCM バッファのみサポートしていましたが、現在はほとんどのフォーマット設定とより広いサンプルレート範囲に対応し、ShazamKit がフォーマット変換を自動で行います。

カスタムカタログで複数の参照シグネチャが類似している場合、ShazamKit はマッチ品質順にすべてのマッチ結果を返します。mediaItems で必要なエントリをフィルタリングできます。

詳細

単発認識

06:46

import ShazamKit

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")
}

キーポイント:

  • SHManagedSession() がマイク権限と録音を自動処理
  • result() は非同期メソッドで、単一の認識結果を返す
  • 結果は match、noMatch、error の 3 状態
  • Info.plist に NSMicrophoneUsageDescription が必須

継続認識

07:16

let managedSession = SHManagedSession()

// 継続的にリッスンし、複数の結果を返す
for await result in managedSession.results {
    switch result {
    case .match(let match):
        print("Match found: \(match.mediaItems.first?.title ?? "")")
    case .noMatch(_):
        print("No match found")
    case .error(let error, _):
        print("Error: \(error.localizedDescription)")
    }
}

キーポイント:

  • results は async sequence で、長時間リスニングに適している
  • cancel() を呼ぶと async sequence が終了する
  • 各マッチ結果は独立して処理され、新しいマッチを継続的に受信できる

完全な Matcher 実装

08: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() {
        isMatching = false
        currentMatchResult = MatchResult(match: nil)
    }
}

キーポイント:

  • カスタムカタログで SHManagedSession(catalog:) を初期化可能
  • 引数なしの初期化はデフォルトの Shazam 音楽カタログを使用
  • @MainActor@Published で SwiftUI が状態変化に自動応答
  • stopRecording()cancel() を呼び出して録音を停止

ウォームアップ準備

10:07

let managedSession = SHManagedSession()

// 事前にウォームアップし、リソースを割り当てて事前録音を開始
await managedSession.prepare()

// ユーザーが認識ボタンをタップしたとき、より速く応答
let result = await managedSession.result()

キーポイント:

  • prepare() でマッチングに必要なリソースを事前割り当て
  • プリレコーディングを開始し、リクエストから結果までの時間を短縮
  • オプション—ShazamKit は必要時に自動で呼び出す

SwiftUI で状態変化に応答

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")
                }
            }
        }
    }
}

キーポイント:

  • session.state.idle.prerecording.matching の 3 種類
  • SHManagedSessionObservable に準拠し、SwiftUI が自動更新
  • idle 状態でボタン、matching 状態でプログレスバーを表示

Shazam Library の読み書き

15:23

import ShazamKit

// メディア項目を Shazam Library に追加
func add(mediaItems: [SHMediaItem]) async throws {
    try await SHLibrary.default.addItems(mediaItems)
}

// SwiftUI で読み取る
struct LibraryView: View {
    var body: some View {
        List(SHLibrary.default.items) { item in
            MediaItemView(item: item)
        }
    }
}

// UI 以外のコンテキストで読み取る
func mostPopularGenre() async -> String? {
    let currentItems = await SHLibrary.default.items
    let genres = currentItems.flatMap { $0.genres }
    return highestOccurringGenre(from: genres)
}

// メディア項目を削除
func remove(mediaItems: [SHMediaItem]) async throws {
    try await SHLibrary.default.removeItems(mediaItems)
}

キーポイント:

  • SHLibrary.default はシングルトンでデバイス間同期
  • addItems で書き込み、items で読み取り、removeItems で削除
  • 本アプリが追加したエントリのみ操作可能
  • Observable に準拠し、SwiftUI List が自動更新

複数マッチ結果のフィルタリング

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 {
// 特定エピソードのメディア項目をフィルタ
        let filteredMediaItems = match.mediaItems.filter { $0.title == "Episode 2" }
        return filteredMediaItems
    }
    
    return []
}

キーポイント:

  • カスタムカタログで類似音声は複数マッチを返す
  • match.mediaItems はマッチ品質順にソート
  • filter でメタデータに基づき必要なエントリを選択
  • カタログ生成時にメタデータを適切に付与し、後のフィルタリングを容易に

重要ポイント

1. 音楽認識ソーシャルアプリ

  • 何を作るか: 友人との集まりで再生中の曲を自動認識し、共有プレイリストを生成
  • なぜ価値があるか: SHManagedSession で数行のコードで認識可能、SHLibrary が全デバイスに自動同期
  • 始め方: SHManagedSession で継続認識し、マッチ成功後に SHLibrary.default.addItems で保存、SwiftUI で SHLibrary.default.items を表示

2. ダンス/フィットネス追従アプリ

  • 何を作るか: 背景音楽を認識し、対応するダンスレッスンやフィットネス指導動画を自動読み込み
  • なぜ価値があるか: デモの「ダンスを学ぶ」シナリオがそのまま使える、Managed Session が録音を自動処理
  • 始め方: SHManagedSession で曲を認識し、match.mediaItems.first から曲情報を取得、曲 ID で動画リソースを検索

3. テレビライブコンテンツ認識

  • 何を作るか: テレビで放送中の番組を認識し、リアルタイムのあらすじや出演者情報を表示
  • なぜ価値があるか: カスタムカタログが複数マッチ返却に対応、同じオープニングでも異なるエピソードを区別可能
  • 始め方: Shazam CLI でテレビ番組カタログを生成し、各エピソードに異なるメタデータを付与、filter で現在のエピソードを選択

4. ライブ公演セットリスト生成器

  • 何を作るか: コンサートやフェスで演奏された各曲を自動記録し、セットリストを生成
  • なぜ価値があるか: results async sequence で公演全体を継続リスニング可能、SHLibrary で記録を保存
  • 始め方: for await result in session.results で継続認識し、重複を除去してタイムスタンプ付きセットリストを生成

関連セッション

コメント

GitHub Issues · utterances