WWDC Quick Look 💓 By SwiftGGTeam
Meet the Music Understanding framework

Meet the Music Understanding framework

观看原视频

Highlight

Apple 推出 Music Understanding 框架,让 App 无需机器学习或信号处理知识,即可在设备端离线分析音频的调性、节奏、结构、速度、乐器活动和响度六个维度,且所有结果数据均可编码为 JSON 导出。

核心内容

以前做音乐分析有多麻烦

做视频剪辑 App 时,一个常见需求是「让画面切换踩上音乐节拍」。以前要实现这个功能,开发者得自己写 FFT(快速傅里叶变换)算法提取音频特征,或者集成 librosa 等 Python 库把音频传到服务器分析。这两种方案都有硬伤:前者需要深厚的信号处理知识,后者依赖网络且存在隐私风险。

Final Cut Pro 团队也面临同样的问题。他们在 macOS 和 iPad 版 FCP 中实现了节拍检测(Beat Detection)和自动蒙太奇(Montage)功能,但这些能力被封装在 FCP 内部,第三方开发者无法复用。

Apple 做了什么

WWDC26 发布的 Music Understanding 框架把 FCP 团队积累的音乐分析能力开放给了所有开发者。框架内部封装了信号处理和模型推理,开发者只需要几行代码就能调用。

01:16)框架提供六个分析维度:

  • 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() 默认分析全部六个维度,计算量较大
  • 整个流程使用 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
  • 建议始终对可选结果做安全解包

时间数据类型

05:53)框架用两个泛型结构体把时间与数据绑定:

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,可直接序列化或跨任务传递
  • 使用 CMTime 而非 Double 避免了浮点精度问题,与 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)结构分析把歌曲拆成三个层级:

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)乐器活动提供两种粒度:

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

关键点:

  • ranges 告诉你某种乐器在哪些时间段出现(布尔判断)
  • 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 推送一次结果
  • 需要配合自定义 AudioProvider 使用
  • 两个任务并发执行:一个消费结果,一个驱动分析

自定义音频提供者

13:19)除了 AVAsset,还可以用实时音频流初始化:

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

    mutating func next() async -> AVReadOnlyAudioPCMBuffer? {
        // 返回下一个音频缓冲区,传 nil 表示结束
    }
}

关键点:

  • AudioProvider 需遵循 AsyncSequenceAsyncIteratorProtocol
  • 每次 next() 返回一个 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)

关键点:

  • 无需手动序列化,一行代码导出全部数据
  • 适合预计算分析结果并随 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 批量分析用户曲库,自动调整每首歌的播放音量,让不同来源的音乐听起来响度一致。这在播客 App 和音乐播放器中都很实用,解决「切歌时被吓一跳」的痛点。

关联 Session

评论

GitHub Issues · utterances