WWDC Quick Look 💓 By SwiftGGTeam
Music Understanding フレームワークを知る

Music Understanding フレームワークを知る

元の動画を見る

ハイライト

Apple は Music Understanding フレームワークを発表しました。App は機械学習や信号処理の知識なしに、デバイス上でオフラインに、調性、リズム、構造、速度感、楽器活動、ラウドネスという 6 つの軸から音声を解析できます。すべての結果データは JSON としてエンコードして書き出せます。

主要内容

以前の音楽解析はどれほど大変だったか

動画編集 App では、「画面切り替えを音楽のビートに合わせる」というよくある要件があります。以前これを実装するには、開発者が自分で FFT、つまり高速フーリエ変換アルゴリズムを書いて音声特徴を抽出するか、librosa などの Python ライブラリを統合して音声をサーバーへ送り解析する必要がありました。どちらにも弱点があります。前者には深い信号処理の知識が必要で、後者はネットワークに依存し、プライバシーリスクもあります。

Final Cut Pro チームも同じ問題に直面していました。彼らは macOS と iPad 版 FCP に Beat Detection と Montage 機能を実装しましたが、その能力は FCP 内部に閉じており、サードパーティ開発者は再利用できませんでした。

Apple が作ったもの

WWDC26 で発表された Music Understanding フレームワークは、FCP チームが蓄積した音楽解析能力をすべての開発者に開放します。フレームワーク内部に信号処理とモデル推論が内包されており、開発者は数行のコードだけで呼び出せます。

01:16)フレームワークは 6 つの解析軸を提供します。

  • Key(調性):曲の主音と調式。例:D flat major
  • Rhythm(リズム):各ビートと小節の正確なタイムスタンプ、および全体 BPM
  • Structure(構造):曲の段落階層。コーラスやヴァースなどの sections、segments、phrases
  • Pace(速度感):音楽の各段落におけるエネルギー密度。値が高いほど速く感じる
  • Instrument Activity(楽器活動):各楽器がいつ現れ、どの程度強いか
  • Loudness(ラウドネス):LUFS 標準に基づく全体ラウドネス、瞬時ラウドネス、ピーク値

00:34)すべての解析はデバイス上で完了し、音声はローカルから出ません。つまりオフライン利用、ネットワーク遅延ゼロ、プライバシー保護が得られます。

この新機能がどう問題を解くか

ビート同期の動画編集機能を例にします。以前は数百行の DSP コードが必要でしたが、今はこれだけです。

  1. 音声ファイルを MusicUnderstandingSession に渡す
  2. analyze() を呼び、リズムと構造データを取得する
  3. 返された CMTime 配列で AVPlayerseek(to:) を直接駆動する

15:28)FCP の Montage 機能もこのように動きます。まず曲の段落を識別し、各段落の pace 値からクリップ長を計算し、動画のリズムを音楽のエネルギーに合わせます。高エネルギー段落では短く速いカット、低エネルギー段落では長くゆったりしたショットを使います。

詳細

Session を初期化する

04:47)フレームワークは MusicUnderstandingSession を通じて音声とやり取りします。最も簡単な初期化方法は AVAsset から作ることです。

import MusicUnderstanding
import AVFoundation

.fileImporter(isPresented: $isPresented, allowedContentTypes: [.audio]) { result in
    switch result {
    case .success(let url):
        let asset = AVURLAsset(
            url: url,
            options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]
        )
        let session = try await MusicUnderstandingSession(asset: asset)
        let results = try await session.analyze()
    case .failure(let error):
        print("読み込みに失敗しました: \(error)")
    }
}

要点:

  • AVURLAssetPreferPreciseDurationAndTimingKey は必ず true に設定します。そうしないとビート合わせに時間ずれが出る可能性があります
  • analyze() はデフォルトで 6 つの軸すべてを解析するため、計算量が大きくなります
  • 全体の流れは async/await を使うため、バックグラウンドタスクで実行することをおすすめします

必要な解析だけを行い性能を上げる

03:45)一部の結果だけが必要な場合は、analyze(for:) を使って不要な計算を避けます。

let results = try await session.analyze(for: [.rhythm, .pace])

if let rhythm = results.rhythm {
    let bpm = rhythm.beatsPerMinute ?? 120.0
    let beatTimes = rhythm.beats
    print("BPM: \(bpm), ビート数: \(beatTimes.count)")
}

要点:

  • analyze(for:) は要求した解析型だけを返し、それ以外のフィールドは nil になります
  • beatsPerMinuteFloat? です。純粋な朗読のように明確なリズムがない音声では nil を返します
  • optional な結果は常に安全に unwrap することをおすすめします

時間データ型

05:53)フレームワークは 2 つのジェネリック構造体で時間とデータを結びつけます。

public struct TimedValue<Value>: Codable, Equatable, Sendable
where Value: Codable & Equatable & Sendable {
    public let time: CMTime
    public let value: Value
}

public struct RangedValue<Value>: Codable, Equatable, Sendable
where Value: Codable & Equatable & Sendable {
    public let range: CMTimeRange
    public let value: Value
}

要点:

  • TimedValue は値を特定の時点に結びつけます
  • RangedValue は値を特定の時間範囲に結びつけます
  • どちらも CodableSendable に準拠し、直接シリアライズしたりタスク間で渡したりできます
  • Double ではなく CMTime を使うことで浮動小数点精度問題を避け、AVFoundation と自然に連携できます

調性解析

06:27)調性解析は曲の主音と調式を返します。

public struct KeyResult: Codable, Sendable {
    public let ranges: [MusicUnderstandingSession.RangedValue<KeySignature>]
}

public struct KeySignature: Codable, Hashable, Sendable {
    public let tonic: Tonic
    public let mode: Mode
}

@frozen public enum Tonic: String, Codable, Hashable, Sendable {
    case aFlat, aSharp, a, bFlat, b, c, cSharp,
         d, dFlat, dSharp, eFlat, e, f, fSharp,
         g, gFlat, gSharp
}

public enum Mode: String, Codable, Hashable, Sendable {
    case major, minor
}

要点:

  • KeyResult.ranges は配列です。曲は段落によって転調することがあるためです
  • RangedValue<KeySignature> は、その調性が有効な時間範囲を含みます
  • Tonic はシャープとフラットを含む 17 種類の音名表記をカバーします

リズム解析

07:16)リズム解析は各ビートと小節の正確なタイムスタンプを返します。

public struct RhythmResult: Codable, Sendable {
    public let beats: [CMTime]
    public let bars: [CMTime]
    public let beatsPerMinute: Float?
}

要点:

  • beats 配列には曲中の各ビートの CMTime が含まれます
  • bars 配列には各小節の開始時刻が含まれます
  • beatsPerMinute は全体平均 BPM で、音声に明確なリズムがない場合は nil です

構造解析

08:42)構造解析は曲を 3 つの階層に分解します。

public struct StructureResult: Codable, Sendable {
    public let sections: [CMTimeRange]
    public let segments: [CMTimeRange]
    public let phrases: [CMTimeRange]
}

要点:

  • sections はコーラス、ヴァース、イントロ、ブリッジなどの大きな段落に対応します
  • segments は sections の下位分割です
  • phrases は最も細かい粒度で、音楽における「句」に相当します
  • 階層関係は section > segment > phrase です

速度感解析

09:26)Pace は音楽が聴き手に与える速度感を表します。

public struct PaceResult: Codable, Sendable {
    public let ranges: [MusicUnderstandingSession.RangedValue<Double>]
}

要点:

  • Pace は BPM とは異なり、「エネルギー密度」という主観的な感覚を測ります
  • 値が高いほど、音楽は速く、活力があるように感じられます
  • 返り値は時間範囲付きの配列であり、段落ごとに異なる pace 値を持つことがあります

14:47)pace で動画クリップ長を計算します。

let timePerClip = 60 / paceValue

楽器活動解析

10:13)楽器活動は 2 種類の粒度を提供します。

public struct InstrumentActivityResult: Codable, Sendable {
    public let ranges: [Instrument: [CMTimeRange]]
    public let activity: [Instrument: [MusicUnderstandingSession.TimedValue<Float>]]
}

要点:

  • ranges は特定の楽器がどの時間帯に現れるかを示します。Boolean 的な存在判定です
  • activity は各時点におけるその楽器の強度を 0 から 1 の浮動小数点値で示します
  • activity データはオーディオ可視化アニメーションの駆動に適しています

ラウドネス解析

11:45)ラウドネスは LUFS(Loudness Units Full Scale)標準で計算されます。

public struct LoudnessResult: Codable, Sendable {
    public let integrated: MusicUnderstandingSession.TimedValue<Float>
    public let momentary: [MusicUnderstandingSession.TimedValue<Float>]
    public let shortTerm: [MusicUnderstandingSession.TimedValue<Float>]
    public let peak: MusicUnderstandingSession.TimedValue<Float>
}

要点:

  • integrated:曲全体の平均ラウドネス。単一の値
  • momentary:100ms ごとのラウドネス。解析窓は 400ms で、突発的な音量変化の検出に向いています
  • shortTerm:100ms ごとのラウドネス。解析窓は 3 秒で、より滑らかな曲線になります
  • peak:曲全体の絶対ピーク音量。単位はデシベルです

ストリーミングラウドネス API

12:48)フレームワークは AsyncSequence スタイルのストリーミングラウドネスデータを提供し、リアルタイム場面に適しています。

let audioProvider = AudioProvider()
let session = MusicUnderstandingSession(audioProvider: audioProvider)

await withThrowingTaskGroup(of: Void.self) { group in
    group.addTask {
        for try await result in await session.loudnessResults {
            updateAudioLevel(result.momentary.value)
        }
    }

    group.addTask {
        try await session.analyze(for: [.loudness])
    }
}

要点:

  • loudnessResultsAsyncSequence で、100ms ごとに結果を push します
  • カスタム AudioProvider と組み合わせて使う必要があります
  • 2 つのタスクが並行して動きます。1 つは結果を消費し、もう 1 つは解析を駆動します

カスタム音声プロバイダ

13:19AVAsset 以外に、リアルタイム音声ストリームで Session を初期化することもできます。

struct AudioProvider: AsyncSequence, AsyncIteratorProtocol {
    func makeAsyncIterator() -> Self {
        return self
    }

    mutating func next() async -> AVReadOnlyAudioPCMBuffer? {
        // 次の音声バッファを返す。終了を示すには nil を渡す
    }
}

要点:

  • AudioProviderAsyncSequenceAsyncIteratorProtocol に準拠する必要があります
  • next() は毎回 1 つの AVReadOnlyAudioPCMBuffer を返します
  • 解析終了時には nil を返し、フレームワークに停止を通知する必要があります
  • マイクのリアルタイム入力やネットワーク音声ストリームなどに適しています

解析結果を書き出す

13:55)すべての結果は Codable に準拠しており、JSON として直接エンコードできます。

let session = try await MusicUnderstandingSession(asset: asset)
let results = try await session.analyze()

let encoder = JSONEncoder()
let data = try encoder.encode(results)

要点:

  • 手動シリアライズは不要で、1 行ですべてのデータを書き出せます
  • 解析結果を事前計算して App に同梱したり、サーバーへアップロードして共有したりする用途に適しています
  • JSON 内のタイムスタンプは CMTime のエンコード形式を使います

主要な学び

1. 「自動ビート同期」動画編集 App を作る

ユーザーが音楽を読み込むと、App がビートと構造を自動解析し、beat と bar の位置にトランジションマーカーを挿入します。開発者は RhythmResult.beats 配列を読み、各 CMTime をタイムライン上のマーカーに変換するだけです。入口 API は MusicUnderstandingSession.analyze(for: [.rhythm, .structure]) です。

2. 「エネルギー可視化」音楽プレイヤーを作る

曲の再生中に、PaceResultInstrumentActivityResult.activity を使って粒子アニメーションや波形を駆動します。高 pace 段落では粒子の動きを速くし、ドラムが現れたときに閃光効果を発火します。TimedValue<Float> のタイムスタンプを AVPlayer の現在時刻と直接比較すれば同期できます。

3. 「スマート DJ」App を作る

KeyResult でユーザーの曲ライブラリの調性を分析し、調性的に相性のよい曲を自動推薦してミックスを作ります。長調から長調、短調から短調、あるいは五度圏で隣接する調性なら自然につなげられます。入口はライブラリを走査して analyze(for: [.key]) を呼び、結果をローカルデータベースに保存することです。

4. 「リズムゲーム」を作る

事前に analyze() で曲のビートデータを計算し、ゲームリソースに同梱します。ゲーム実行時には beats 配列を読み、該当時刻にタップターゲットを生成します。実行時リアルタイム解析より省性能で、ゲーム中に解析失敗するリスクも避けられます。

5. 「ラウドネス均一化」ツールを作る

LoudnessResult.integrated でユーザーの曲ライブラリを一括解析し、各曲の再生音量を自動調整して、異なるソースの音楽が同じくらいの大きさに聞こえるようにします。Podcast App や音楽プレイヤーで有用で、「曲が変わった瞬間に驚く」問題を解決します。

関連 Session

  • Meet MusicKit for Swift — MusicKit は音楽再生とライブラリ管理機能を提供し、Music Understanding の解析能力と組み合わせることで完全な音楽 App を構築できる
  • Create 3D models for your spatial app — 3D モデルで空間オーディオ可視化シーンを構築し、Music Understanding の解析結果を 3D 空間へマッピングする
  • What’s new in SwiftUI — SwiftUI の Canvas とアニメーション API は、Music Understanding Lab 風のリアルタイム可視化インターフェイスを描くのに適している
  • Explore machine learning on Apple platforms — Apple デバイス上 ML の基盤メカニズムを学び、Music Understanding フレームワークの推論最適化戦略を理解する

コメント

GitHub Issues · utterances