Sleep Cycle SDK

iOS SDK

面向 iOS 应用的高级睡眠分析能力

Sleep Cycle SDK - iOS 文档

概览

Sleep Cycle SDK for iOS 让开发者能够将先进的睡眠分析能力集成到他们的应用中。SDK 通过音频和运动传感器提供实时睡眠追踪,并在整夜过程中生成详细的睡眠洞察、睡眠阶段转换以及检测到的事件。

系统要求

最低 iOS 版本:

  • iOS:16.0+
  • macOS:13.0+

Swift:

  • Swift 版本:5.9+
  • SDK 使用 Swift 编写,并提供原生的 Swift API,支持 async/await

安装

该 Swift 包提供两个由同一份源码构建的 product,二者都提供 SleepCycleSDK 模块。请只依赖其中之一:

SleepCycleSDKSleepCycleSDKSlim
机器学习模型内嵌在 framework 中首次 initialize(...) 时下载
嵌入到 App 中的 framework 体积约 16 MB约 1.6 MB
首次 initialize(...)无需下载下载已获授权的功能所需的模型
网络要求仅授权时需要授权与首次下载时需要

两个 product 同步发版,API 完全一致——无论选择哪一个,都写 import SleepCycleSDK,调用方式也相同,因此切换只需改动一行包依赖。若希望减小应用下载体积,并可以接受首次启动时一次性的模型下载,请选择 SleepCycleSDKSlim;若要求 SDK 除授权外无需下载任何内容即可使用,请选择 SleepCycleSDK。

Swift Package Manager

使用 Swift Package Manager 将 Sleep Cycle SDK 添加到您的项目:

  1. 在 Xcode 中选择 File → Add Package Dependencies
  2. 输入 package URL:https://github.com/MDLabs/sleepcycle-sdk-swift
  3. 选择您要使用的版本(语义化版本号)
  4. 将 SleepCycleSDK 或 SleepCycleSDKSlim 库 product 之一添加到您的目标

也可以将其添加到 Package.swift 中:

dependencies: [
    .package(url: "https://github.com/MDLabs/sleepcycle-sdk-swift", from: "1.3.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            // 模型内嵌在 framework 中
            .product(name: "SleepCycleSDK", package: "sleepcycle-sdk-swift")

            // 模型在运行时下载(替换上面一行,不要同时使用)
            // .product(name: "SleepCycleSDKSlim", package: "sleepcycle-sdk-swift")
        ]
    )
]

两个 product 都提供 SleepCycleSDK 模块,因此 import 保持不变:

import SleepCycleSDK

SDK 需要 API 密钥用于授权。请联系 Sleep Cycle 获取凭证。

使用精简版 product

使用 SleepCycleSDKSlim 时,initialize(...) 会下载您的 API 密钥所授权功能需要的加密模型,并在全部模型在本地就绪之前不会返回。只有必需的模型会被下载,因此启用的功能越少,下载量越小。

下载的模型保存在应用的 Application Support 目录下,并在之后每次启动时复用,因此每次安装只会下载一次(若 SDK 升级更换了模型,则会再次下载)。删除应用或调用 SleepCycleSdk.clear() 时,这些模型会被移除。

可通过可选的 onDownloadProgress 回调向用户展示进度:

let features = try await SleepCycleSdk.initialize(
    logLevel: .info,
    apiKey: "your-api-key-here",
    onDownloadProgress: { progress in
        // progress 为 0...1 之间的比例,从后台上下文回调
        Task { @MainActor in self.downloadProgress = progress }
    }
)

内嵌模型的 product 永远不会调用该回调,因此同一份代码对两者均无需改动。

资源就绪失败会以 SleepCycleSdkError 的形式从 initialize(...) 抛出:

.resourceUnavailable(_:) - 所需模型没有下载位置,例如使用了在支持模型下载之前签发的令牌
.resourceDownloadFailed(_:) - 下载失败,例如首次启动时设备处于离线状态
.modelNotProvisioned(_:) - 分析所需的模型尚未下载
.modelIntegrityCheckFailed(_:) - 已下载的模型不完整或已损坏
.modelUnavailable(_:) - 该模型不属于此 SDK 构建

下载失败是可以恢复的:待设备联网后再次调用 initialize(...) 即可。下载会从已有的部分文件断点续传,重试不会从头开始。

前置条件

权限

SDK 需要麦克风权限以进行基于音频的睡眠分析。请将以下内容添加到您的 Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>We need access to the microphone to analyze your sleep patterns and detect snoring.</string>

如需基于运动的分析,您可能还需要:

<key>NSMotionUsageDescription</key>
<string>We use motion data to track your sleep movements.</string>

后台模式

要确保整夜持续进行睡眠分析,请在应用的 capabilities 中启用相应的后台模式:

  1. 在 Xcode 中打开您的项目
  2. 选择应用的 target
  3. 进入 "Signing & Capabilities"
  4. 添加 "Background Modes" 能力
  5. 启用 "Audio"

也可以将以下内容添加到 Info.plist:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

SDK 在睡眠分析期间使用音频后台模式来维持持续的音频处理。您的应用还应实现合适的会话管理,以防止 iOS 挂起分析进程。

通用说明

SDK 是线程安全的,所有异步操作均使用 Swift 并发(async/await)。

初始化 SDK

SDK 在使用前需要进行身份验证。初始化过程会校验您的凭证并确定可用的功能。

import SleepCycleSDK

Task {
    do {
        let features = try await SleepCycleSdk.initialize(
            logLevel: .info,
            apiKey: "your-api-key-here"
        )
        print("Authorized with features: \(features)")
    } catch {
        print("Initialization error: \(error)")
    }
}

参数:

logLevel LogLevel - SDK 日志详细程度(.debug、.info、.warning、.error、.noLog)
logger Logger? - 可选的自定义日志记录器(默认为系统日志记录器)
apiKey String - 您的 Sleep Cycle API 密钥
forceTokenRefresh Bool - 即使存在缓存令牌也强制重新获取令牌(默认为 false)
telemetry TelemetryConfig - 控制 SDK 发送哪些遥测数据(默认关闭分析数据,开启错误报告)
onDownloadProgress ((Double) -> Void)? - 可选的模型下载进度,取值为 0...1 之间的比例,从后台上下文回调

使用 SleepCycleSDKSlim 时,initialize(...) 还会下载已获授权的功能所需的模型,并在它们于本地就绪之前不会返回。参见使用精简版-product。

遥测数据

telemetry 参数控制 SDK 向 Sleep Cycle 回报哪些数据:

analytics Bool - 睡眠会话分析事件。默认关闭
errorReporting Bool - 描述技术故障的诊断报告。默认开启

let features = try await SleepCycleSdk.initialize(
    logLevel: .info,
    apiKey: "your-api-key-here",
    telemetry: TelemetryConfig(analytics: true, errorReporting: true)
)

两个开关互相独立。当 analytics 关闭时,不会有任何分析事件被排队或发送。当 errorReporting 关闭时,不会发送任何诊断报告,但故障信息仍会传递给您在 initialize(...) 中传入的 Logger。

分析事件反映 SDK 在您的用户实际使用的设备上的表现,使 Sleep Cycle 得以发现影响特定设备型号的异常并据此调整分析——例如麦克风差异如何影响声压级测量,或如何影响睡眠分期与呼吸频率检测。若您的隐私政策有此要求,将 analytics 保持关闭即可;两种设置下 SDK 的行为完全相同。

单个事件不携带任何设备或用户标识符。它记录所属的会话、发生时间、设备型号、操作系统版本与 SDK 版本,因此无法将某次会话与某个安装或任何其他会话关联起来。

返回的 SleepAnalysisFeatures 表示您的 API 密钥可用的能力:

sleepStaging Bool - 睡眠分期分析
smartAlarm Bool - 智能闹钟功能
audioEvents Bool - 音频事件检测
snoringDetection Bool - 鼾声检测
realTimeSleepStaging Bool - 实时睡眠分期
multiChannelAnalysis Bool - 多声道分析(立体声,两个声道)
extendedAudioEvents Bool - 标准集之外的扩展音频事件类型

在运行时访问功能开关:

if SleepCycleSdk.isFeatureEnabled(\.audioEvents) {
    // Present snore/talk event UI
}

反初始化与清除 SDK

deinitialize() 会释放 SDK 的资源并将其恢复到未初始化状态:

await SleepCycleSdk.deinitialize()

它会停止数据源,但不会结算或结束活动会话,因此被应用关闭中断的会话可以再次通过 resumeAnalysis(...) 接管。若要结束这些会话,请先调用 stopAnalysis(at:) 或 stopDataSource(),或事后调用 clearActiveSessions()。

clear() 更进一步:它会反初始化 SDK 并清除持久化状态——缓存的授权令牌,以及精简版 product 下载的所有模型:

await SleepCycleSdk.clear()

下一次 initialize(...) 会重新授权;使用精简版 product 时,还会重新下载所需的模型。SleepCycleSDK 中内嵌的模型不受影响。

获取 SDK 状态

通过 AsyncStream 监听 SDK 状态的变化:

import SleepCycleSDK

Task {
    for await state in await SleepCycleSdk.stateStream {
        switch state {
        case .uninitialized:
            print("SDK not initialized")
        case .initialized:
            print("SDK ready")
        case .running:
            print("Analysis in progress")
        }
    }
}

获取当前状态:

let currentState = await SleepCycleSdk.currentState

启动睡眠分析会话

初始化完成后,即可启动一次睡眠分析会话。该方法返回标识本次会话的 UUID。

import SleepCycleSDK

Task {
    do {
        let sessionId: UUID = try await SleepCycleSdk.startAnalysis(
            config: SleepAnalysisConfig(
                useAudio: true,
                useAccelerometer: true
            )
        )
        print("Analysis started with session ID: \(sessionId)")
    } catch {
        print("Failed to start analysis: \(error)")
    }
}

参数:

config SleepAnalysisConfig - 用于指定使用哪些传感器的配置对象
at Date - 分析的起始时间(默认为当前时间)
using DataSource? - 可选的数据源(例如实时或文件回放)
eventListeners [AudioEventListener] - 可选的监听器数组,在音频分析过程中接收回调。可用于实时捕获音频样本和事件

恢复会话

SDK 支持恢复先前启动的分析会话。当应用重启或后台任务被系统终止时,这一功能很有用。

if await SleepCycleSdk.isResumePossible() {
    let resumedSessions: [ResumedSession] = try await SleepCycleSdk.resumeAnalysis()
    for session in resumedSessions {
        print("Resumed \(session.sessionId) on channel \(session.channel)")
    }
}

resumeAnalysis() 返回一个 ResumedSession 数组,每个元素包含所恢复会话的 sessionId、channel 以及任何已持久化的 smartAlarmConfig。

若某个可恢复的会话不打算恢复,可用 clearActiveSessions() 结束它并丢弃其结果,使 isResumePossible() 重新返回 false:

try await SleepCycleSdk.clearActiveSessions()

若数据源正在运行,该方法会抛出 SleepCycleSdkError.analysisAlreadyRunning。

停止会话

要停止当前的分析会话并获取结果:

Task {
    do {
        let result = try await SleepCycleSdk.stopAnalysis()

        print("Session ID: \(result.sessionId)")

        if let statistics = result.statistics {
            print("Total sleep duration: \(statistics.totalSleepDuration ?? 0)")
            print("Sleep efficiency: \(statistics.sleepEfficiency ?? 0)")

            if let snoreSessions = statistics.snoreSessions {
                for session in snoreSessions {
                    print("Snoring session: \(session.interval)")
                }
            }
        }

        for event in result.events {
            print("\(event.type) detected: \(event.interval), p=\(event.probability)")
        }

        for breathingRate in result.breathingRates {
            print("Breathing rate: \(breathingRate.bpm) bpm at \(breathingRate.timestamp)")
        }

        for stageInterval in result.sleepStageIntervals {
            print("\(stageInterval.stage): \(stageInterval.interval)")
        }

        if let audioStats = result.audioStatistics {
            for interval in audioStats.healthIntervals {
                print("Audio \(interval.status): \(interval.interval)")
            }
        }
    } catch {
        print("Failed to stop analysis: \(error)")
    }
}

分析结果

AnalysisResult 包含一次睡眠分析会话的完整输出:

sessionId UUID - 唯一会话标识符
startTime Date - 会话起始时间
endTime Date - 会话结束时间
timeZone TimeZone - 会话开始时捕获的时区
events [Event] - 检测到的睡眠事件
breathingRates [BreathingRate] - 呼吸频率测量值
sleepStageIntervals [SleepStageInterval] - 睡眠阶段数据
realTimeSleepStageIntervals [SleepStageInterval] - 会话期间实时发出的睡眠分期
sleepStagePredictions [SleepStagePrediction] - 会话结束后对每个 epoch 的预测,包含各睡眠阶段的概率
statistics SleepStatistics? - 聚合的睡眠统计(可选)
audioStatistics AudioStatistics? - 音频运行状况统计(可选)

SleepStatistics

当存在时,statistics 包含本次睡眠会话的聚合指标:

totalSleepDuration Double? - 总睡眠时长
sleepOnsetLatency Double? - 入睡所用时间
sleepEfficiency Double? - 睡眠时长与在床时长的比值(0.0 到 1.0)
finalWakeTime Date? - 最终醒来时间
numberOfAwakenings Int? - 整夜的醒来次数
snoreTime Double? - 鼾声总时长
snoreSessions [SnoreSession]? - 单次鼾声会话
sleepStageDurations [SleepStage: Double]? - 每个睡眠阶段的时长

AudioStatistics

当存在时,audioStatistics 包含整个会话期间音频输入运行状况的相关信息。

睡眠评分

会话完成后,您可以使用 SleepScoring.compute(result:history:dateOfBirth:chronoType:) 计算当晚的睡眠评分。该评分将当晚的结果与最近几晚的简短历史记录结合起来。

import SleepCycleSDK

// 从每个已完成的结果构建历史记录条目,并将其持久化以供后续夜晚使用
let historyEntry = result.toSleepScoreHistoryEntry()

let score = SleepScoring.compute(
    result: result,
    history: recentHistoryEntries,
    dateOfBirth: userDateOfBirth,   // 可选,按年龄调整质量子评分
    chronoType: userChronoType      // 可选,选择时段子评分的窗口
)

print("Sleep score: \(score.total) (duration=\(score.duration), quality=\(score.quality), routine=\(score.routine))")

每个 SleepScore 字段的取值范围为 0.0–1.0,数值越高越好:

total Float - 当晚的总体睡眠评分
duration Float - 用户的睡眠时长
quality Float - 用户的睡眠质量
routine Float - 用户的睡眠作息

SleepScoring.identifyChronoType(history:) 根据最近的历史记录推导用户的 ChronoType(.extremeMorning、.morning、.intermediate、.evening、.extremeEvening),数据不足时返回 nil。其结果可作为 chronoType 参数传回 compute(...)。

实时事件

SDK 在分析过程中通过 AsyncStream API 提供实时事件更新:

Task {
    for await events in await SleepCycleSdk.eventStream {
        for event in events {
            switch event.type {
            case .movement:
                handleMovement(event)
            case .snoring:
                handleSnoring(event)
            case .talking:
                handleTalking(event)
            case .coughing:
                handleCoughing(event)
            default:
                handleOtherEvent(event)
            }
        }
    }
}

当您的 API 密钥启用了 extendedAudioEvents 功能时,会在标准事件之外同时报告额外的 EventType 值:.bird、.cat、.digestive、.dog、.fart、.music、.sneeze、.throatClearing、.traffic、.water 和 .wind。使用 EventType.standardTypes 和 EventType.extendedTypes 来区分这两个集合。

每个 Event 包含:

type EventType - 事件类型
interval DateInterval - 事件的时间区间
probability Double - 置信度分数(0.0 到 1.0)
source EventSource - 检测来源
sessionId UUID - 该事件所属的会话
signature [Float]? - 可选特征向量(用于鼾声事件)

实时呼吸频率

SDK 在分析过程中提供实时呼吸频率测量:

Task {
    for await breathingRate in await SleepCycleSdk.breathingRateStream {
        print("Breathing rate: \(breathingRate.bpm) bpm (confidence: \(breathingRate.confidence))")
    }
}

每个 BreathingRate 包含:

timestamp Date - 测量记录的时间
bpm Double - 呼吸频率(每分钟呼吸次数)
confidence Double - 测量的置信度(0.0 到 1.0)
sessionId UUID - 该测量所属的会话

实时睡眠分期

SDK 可在分析过程中提供实时睡眠阶段预测。此功能要求您的 API 密钥启用了 realTimeSleepStaging 功能。

Task {
    for await prediction in await SleepCycleSdk.sleepStageStream {
        switch prediction.stage {
        case .awake:
            print("Awake: \(prediction.interval)")
        case .light:
            print("Light sleep: \(prediction.interval)")
        case .deep:
            print("Deep sleep: \(prediction.interval)")
        case .rem:
            print("REM sleep: \(prediction.interval)")
        }
        print("Deep sleep probability: \(prediction.probabilities.deep)")
    }
}

该流在分析过程中大约每 30 秒发出一个 SleepStagePrediction 对象,从而以接近实时的方式反馈睡眠状态的转换。每个预测都包含最可能的睡眠阶段 stage、对应 epoch 的时间区间 interval,以及 probabilities(一个 SleepStageProbabilities,包含 awake、rem、light 和 deep 各睡眠阶段的概率)。这些概率之和为 1,可以通过 probabilities[.deep] 按睡眠阶段读取。

实时音频运行状况

SDK 在分析期间监测音频输入的运行状况,并在状态变化时发出更新:

Task {
    for await update in await SleepCycleSdk.audioHealthStream {
        switch update.status {
        case .healthy:
            print("Audio input healthy")
        case .flatline:
            print("Audio flatline detected")
        case .missingInput:
            print("Audio input missing")
        }
    }
}

AudioHealthStatus 取值:

.healthy - 音频输入包含有变化的信号
.flatline - 检测到恒定值(麦克风失效或输入静音)
.missingInput - 较长时间内未收到任何音频输入

智能闹钟

智能闹钟在唤醒时段内监测身体活动,并在用户处于浅睡眠阶段时发出事件,从而让您在最佳时机唤醒用户。这需要为您的 API 密钥启用 smartAlarm 功能,且仅在主声道上支持。

使用 SmartAlarmConfig 配置唤醒时段,并在开始分析时传入:

import SleepCycleSDK

let wakeupWindow = DateInterval(start: windowStart, end: windowEnd)

try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: true),
    smartAlarmConfig: SmartAlarmConfig(wakeupWindow: wakeupWindow)
)

通过 smartAlarmStream 观察闹钟生命周期事件:

Task {
    for await event in await SleepCycleSdk.smartAlarmStream {
        switch event.type {
        case .armed:
            print("Smart alarm armed")
        case .triggered(let reason):
            switch reason {
            case .optimalWakeUp, .userInteraction, .windowEnd:
                ringAlarm()
            }
        }
    }
}

可在会话运行期间使用 updateSmartAlarm(config:) 修改闹钟。它会替换会话启动时设置的闹钟、为未设置闹钟的会话添加闹钟,或在传入 nil 时取消闹钟:

// 调整唤醒时段
try await SleepCycleSdk.updateSmartAlarm(
    config: SmartAlarmConfig(wakeupWindow: DateInterval(start: newStart, end: newEnd))
)

// 取消闹钟
try await SleepCycleSdk.updateSmartAlarm(config: nil)

新配置会被持久化,因此应用被终止后恢复的会话会沿用它;已经触发过的闹钟会被新配置重新装载,从而可以再次触发。若唤醒时段已经过去,会在下一次分析节拍时以 .windowEnd 触发。该调用针对 .primary 会话,若该声道上没有活动会话,则抛出 SleepCycleSdkError.analysisNotActive。

smartAlarmConfig 参数在 startMultiChannelAnalysis(...) 上同样可用,且该方法会拒绝为 .primary 以外的声道传入该参数。

事件签名

对于鼾声事件,Event.signature 属性包含一个 16 维特征向量,表示所检测到鼾声的独特特征。来自同一人的鼾声事件在签名空间中彼此聚集,因此可以按人对事件进行聚类。

音频事件监听器

AudioEventListener 协议允许您在会话过程中接收实时的音频分析更新。实现该协议可在分析进行时访问原始音频样本、事件检测以及音量信息。

class MyAudioListener: AudioEventListener {
    func onAudioAnalysisBatchCompleted(
        sessionId: UUID,
        audioSamples: [Float],
        audioSampleRate: Int,
        audioStartTime: Date,
        audioEndTime: Date,
        audioProbability: [EventType: Float],
        eventsStarted: [EventStartedInfo],
        eventsEnded: [EventEndedInfo],
        dbSpl: [Float]
    ) {
        // Process audio samples and events
    }
}

let listener = MyAudioListener()
try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true),
    eventListeners: [listener]
)

audioSamples 参数顺序包含所有已处理的音频数据,批次之间没有间隙或重叠。每个批次都从前一个批次结束的位置精确接续,从而对所有分析过的音频实现完整覆盖。

audioProbability 参数提供所分析时间窗口内每种 EventType 的每批次概率。dbSpl 参数提供批次中每个时间帧以 dB SPL 表示的 A 计权声音音量。

音频片段

当检测到特定的睡眠事件(例如鼾声、梦话或咳嗽)时,SDK 可以捕获短音频录音。

使用 AudioClipsConfig 和 AudioClipsReceiver 创建一个 AudioEventListener:

import SleepCycleSDK

let listener = SleepCycleSdk.createAudioClipsProducer(
    audioClipsConfig: AudioClipsConfig(
        activeTypes: [
            .snoring: EventTypeConfig(minDuration: 0.5),
            .talking: EventTypeConfig(minDuration: 0.5)
        ],
        clipLength: 5.0
    ),
    receiver: MyAudioHandler()
)

try await SleepCycleSdk.startAnalysis(
    config: SleepAnalysisConfig(useAudio: true),
    eventListeners: [listener]
)

每个 AudioClip 包含:

startTime Date - 起始时间戳
type EventType - 触发本次捕获的事件类型
samples [Float] - 原始音频样本
sampleRate Int - 采样率(Hz)
sessionId UUID - 该片段所属的会话

多声道分析

SDK 支持使用立体声音频源同时分析两个声道。多声道分析将数据源生命周期与各个会话生命周期分离,使您可以独立启动和停止每个声道上的会话。

立体声流应来自两个独立的单声道麦克风,每个麦克风占用一个声道,再合并为单个立体声流。

此功能要求您的 API 密钥启用了 multiChannelAnalysis 功能。

声道分离

使用立体声输入时,ChannelSeparationConfig 控制如何将音频事件分配到各个声道。内置预设:

  • .bedSideMics — 分离放置的床头麦克风(默认值)
  • .centeredMicArray — 间距很近的麦克风阵列
  • .detectionStrengthOnly() — 不进行空间过滤,仅依据检测置信度

所有参数(麦克风间距、模糊区、置信度阈值、各事件类型设置)都可以单独调节,以适配您的具体硬件配置和使用场景。

数据源生命周期

在启动各个会话之前,使用立体声音频配置启动数据源:

import SleepCycleSDK

let dataSource = SleepCycleSdk.createLiveDataSource()

try await SleepCycleSdk.startDataSource(
    using: dataSource,
    channelSeparationConfig: .bedSideMics
)

在每个声道上启动会话

数据源运行后,在每个声道上启动一个会话:

let primarySessionId: UUID = try await SleepCycleSdk.startMultiChannelAnalysis(
    channel: .primary,
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: true)
)

let secondarySessionId: UUID = try await SleepCycleSdk.startMultiChannelAnalysis(
    channel: .secondary,
    config: SleepAnalysisConfig(useAudio: true, useAccelerometer: false)
)

AnalysisChannel 取值:

.primary - 第一个音频声道(或单声道)
.secondary - 立体声中的第二个音频声道

独立停止各个会话

每个会话都可以独立停止以获取其结果:

let primaryResult = try await SleepCycleSdk.stopAnalysis(sessionId: primarySessionId)
let secondaryResult = try await SleepCycleSdk.stopAnalysis(sessionId: secondarySessionId)

停止数据源

所有会话都已停止后,再停止数据源:

await SleepCycleSdk.stopDataSource()

如果在仍有会话处于活动状态时调用 stopDataSource(),会强制停止这些会话并丢弃其结果。要保留结果,请先停止每个会话。

On this page