IOS音视频(四十三)AVFoundation 之 Audio Session

@[TOC](IOS音视频(四十三)AVFoundation 之 Audio Session)

1.音频会话概述

音频是iOS、tvOS和watchOS中的托管服务。系统通过使用音频会话来管理应用程序、应用程序间和设备级别上的音频行为。

音频会话

你使用一个音频会话来与系统沟通,告诉系统你打算如何在你的应用程序中使用音频。这个音频会话充当了你的应用程序和操作系统之间的媒介,反过来,也就是底层音频硬件之间的媒介。你可以使用它来与操作系统交流应用程序音频的性质,而无需详细说明具体的行为或与音频硬件的必要交互。将这些细节的管理委托给音频会话可以确保对用户音频体验的最佳管理。

你与你的应用程序的音频会话使用AVAudioSession的一个实例:

  1. 配置音频会话类别和模式,以向系统传达您打算如何在应用程序中使用音频
  2. 激活你的应用程序的音频会话,把你的类别和模式配置到行动
  3. 订阅和响应重要的音频会话通知,如音频中断和路由更改
  4. 执行高级音频设备配置,如设置采样速率、I/O缓冲区持续时间和通道数量
  • 音频会话管理音频行为
    音频会话是应用程序和操作系统之间的中介,用于配置应用程序的音频行为。在启动时,你的应用程序会自动提供一个单例音频会话。您可以配置它来提供所需的行为,并激活它来将该行为转化为操作。

  • 类别表示音频角色

表达音频行为的主要机制是音频会话类别。通过设置类别,您可以指示应用程序是否使用输入或输出路径,是否希望音乐与音频一起继续播放,等等。你指定的行为应该满足用户的期望,正如在iOS人机界面指南音频中描述的那样。

AVFoundation定义了许多音频会话类别,以及一组覆盖和修改开关,允许您根据应用程序的个性或角色自定义音频行为。各种类别支持回放、录制和带有录制的回放。当系统知道您的应用程序的音频角色时,它将为您提供对硬件资源的适当访问。该系统还确保设备上的其他音频以适合您的应用程序的方式工作,并符合用户的期望。

某些类别还可以通过指定用于专门化给定类别的行为的模式来进一步定制。例如,当应用程序使用视频录制模式时,系统可能会选择不同于使用默认模式时的内置麦克风。该系统还可能采用为视频录制用例进行调优的麦克风信号处理。

  • 通知支持中断处理
    音频中断是应用程序的音频会话的停用,它会立即停止音频。当来自应用程序的竞争音频会话被激活,并且该会话未被系统分类以与您的会话混合时,就会出现中断。您的应用程序应该通过保存状态、更新用户界面等方式来响应中断。要在音频中断开始和结束时得到通知,请注册以观察AVAudioSessionInterruptionNotification类型的通知。

  • 通知支持音频路由更改处理
    当用户通过连接或断开设备、插入或断开耳机来启动音频路由更改时,他们有特定的期望。iOS人机界面指南描述了这些期望,并提供了如何满足这些期望的指南。通过注册来观察AVAudioSessionRouteChangeNotification类型的通知来处理路由更改。

  • 音频会话控制设备配置
    应用程序不能直接控制设备硬件,但音频会话为您提供了请求首选硬件设备设置的接口。这个接口使您能够执行高级音频设备配置,如设置采样速率、I/O缓冲区持续时间和音频通道的数量。

  • 音频会话保护用户隐私
    单独或与视频一起录制音频的应用程序,在允许录制之前,需要明确的用户许可。在用户授予你的应用录制权限之前,应用只能录制静音。AVAudioSession提供了请求此权限并确定用户隐私设置的接口。

2. 配置音频会话

音频会话类别是一个键,用于识别应用程序的一组音频行为。通过设置一个类别,您可以向系统表明您的音频意图——例如,当铃声/静音开关翻转时,音频是否应该继续。几个音频会话类别,以及一组覆盖和修改开关,让您自定义您的应用程序的音频行为。

如表B-1所示,每个音频会话类别都指定了一组特定的响应,以响应下列每个行为:

  1. 中断非混合应用程序音频:如果是,当你的应用程序激活其音频会话时,非混合应用程序被中断。
  2. 静音开关静音:如果是,当用户激活静音开关时,您的音频将被静音。(在iPhone上,这个开关叫做铃声/静音开关。)
  3. 支持音频输入:如果支持,则允许app音频输入(录制)。
  4. 支持音频输出:如果是,应用程序音频输出(播放)是允许的。

大多数应用程序只需要在启动时设置类别一次,但你可以根据需要随时更改类别。你可以改变它,而音频会话是积极的;然而,在更改类别或其他会话属性之前,通常更可取的做法是禁用音频会话。在会话被停用时进行这些更改可以防止音频系统进行不必要的重新配置。

2.1 音频会话默认行为

所有的iOS、tvOS和watchOS应用程序都有一个预设的默认音频会话,如下所示:

  • 支持音频播放,但不允许录制音频。
  • 在iOS系统中,将铃声/静音开关设置为静音模式,应用程序播放的任何音频都会被静音。
  • 在iOS系统中,当设备被锁定时,应用程序的音频会被静音。
  • 当您的应用程序播放音频时,任何其他背景音频(如音乐应用程序播放的音频)都将被静音。

默认的音频会话具有有用的行为,但在大多数情况下,您应该自定义它以更好地满足您的应用程序的需要。要更改行为,需要配置应用程序的音频会话。

2.2 配置音频会话

配置音频会话的主要方法是设置其类别。音频会话类别定义了一组音频行为。与每个类别相关的精确行为并不在应用程序的控制之下,而是由操作系统设置的。苹果可能会在未来的操作系统版本中完善类别行为,所以你最好的策略是选择最准确地描述你想要的音频行为意图的类别。音频会话类别和模式总结了每个类别的行为细节。

虽然类别设置了应用程序的基本音频行为,但您可以通过设置类别的模式进一步专门化这些行为。例如,IP语音(VoIP)应用程序将使用AVAudioSessionCategoryPlayAndRecord。您可以通过将音频会话模式设置为AVAudioSessionModeVoiceChat来专门化VoIP应用程序的此类行为。这种模式确保通过系统提供的信号处理来优化语音信号。

AVAudioSessionCategoryPlayAndRecord模式如下:

static let playAndRecord: AVAudioSession.Category
  1. 您的音频继续与静音开关设置为静音和与屏幕锁定。(这个开关在iPhone上被称为铃声/静音开关。)为了在你的应用程序转换到背景时(例如,当屏幕锁定时)继续播放音频,在你的信息属性列表文件的UIBackgroundModes键中添加音频值。
  2. 这个类别适用于同时录制和回放,也适用于录制和回放但不是同时播放的应用程序。
  3. 默认情况下,使用这个类别意味着你的应用程序的音频是不可混合的——激活你的会话将中断任何其他不可混合的音频会话。要允许混合此类别,请使用mixWithOthers选项。
  4. 用户必须为音频录制授予权限(参见录制需要用户权限)。
    这个类别支持Airplay的镜像版本。然而,如果AVAudioSessionModeVoiceChat模式用于此类别,则AirPlay镜像将被禁用。
  5. 某些类别支持通过在会话上设置一个或多个类别选项来覆盖其默认行为(参见AVAudioSessionCategoryOptions)。例如,当会话被激活时,与AVAudioSessionCategoryPlayback类别相关联的默认行为会中断其他系统音频。在大多数情况下,回放应用程序需要这种行为。但是,如果您希望您的音频与其他系统音频混合,您可以通过在会话中设置AVAudioSessionCategoryOptionMixWithOthers选项来覆盖此行为。
  • 音频会话模式有:


    音频会话模式有

注意:当铃声/静音开关设置为静音并锁定屏幕时,为了让你的应用程序继续播放音频,请确保UIBackgroundModes音频键已添加到你的应用程序的信息中。plist文件。这个要求是除了你使用正确的类别。

  • 模式及相关类别:


    模式及相关类别

    要设置音频会话类别(以及可选的模式和选项),请调用setCategory:mode:options:error: method,使用AVFoundation框架设置音频会话类别代码如下:

// Access the shared, singleton audio session instance
let session = AVAudioSession.sharedInstance()
do {
    // Configure the audio session for movie playback
    try session.setCategory(AVAudioSessionCategoryPlayback,
                            mode: AVAudioSessionModeMoviePlayback,
                            options: [])
} catch let error as NSError {
    print("Failed to set the audio session category and mode: \(error.localizedDescription)")
}

2.3 使用多路由类别扩展选项

多路由类别的工作方式与其他类别略有不同。所有其他类别的设备都遵循“最后入网”规则,即最后插入输入或输出路由的设备是占主导地位的设备。但是,多路由类允许应用程序使用所有连接的输出端口,而不是仅使用最后一个端口。例如,如果您正在通过HDMI输出路径听音频,并插入一组耳机,您的应用程序将继续通过HDMI输出路径播放音频,同时也通过耳机播放音频。

使用multiroute类别,您的应用程序还可以将不同的音频流发送到不同的输出路径。例如,您的应用程序可以将一个音频流发送到左侧耳机,另一个发送到右侧耳机,第三个发送到HDMI路由。图1-1显示了将多个音频流发送到不同音频路由的示例。

发送不同的音频流到不同的音频路由

根据设备和任何连接的附件,以下是有效的输出路径组合:

  • USB和耳机
  • HDMI和耳机
  • 列队争球和耳机

多路由类别支持使用单个输入端口。

重要提示:只有在没有其他合适的输出端口(USB、HDMI、LineOut)连接时才能使用内置扬声器。

2.4 为AirPlay选择类别和模式

只有特定的类别和模式支持AirPlay。以下类别同时支持镜像和非镜像版本的AirPlay:

AVAudioSessionCategorySoloAmbient
AVAudioSessionCategoryAmbient
AVAudioSessionCategoryPlayback

AVAudioSessionCategoryPlayAndRecord类别和以下模式只支持镜像版本的AirPlay:

AVAudioSessionModeDefault
AVAudioSessionModeVideoChat
AVAudioSessionModeGameChat

注意:从ios10开始,当你使用AVAudioSessionCategoryPlayAndRecord目录时,你可以通过AVAudioSessionCategoryOptionAllowAirPlay选项激活你的会话来启用非镜像AirPlay输出。

AVAudioSessionCategoryOptionAllowAirPlay = 0x40

  1. 只有在音频会话类别为AVAudioSessionCategoryPlayAndRecord时,才能显式设置此选项。对于大多数其他音频会话类别,系统将隐式地设置此选项。使用AVAudioSessionCategoryMultiRoute或AVAudioSessionCategoryRecord类别的音频会话隐式地清除此选项。
  2. 如果您清除此选项,AirPlay设备将不会显示为可用的音频输出路由。如果设置此选项,这些设备将显示为可用的输出路由。

2.5 开启后台音频

iOS和tvOS应用程序要求您为某些后台操作启用某些功能。回放应用程序需要的一个常见功能是播放背景音频。启用这个功能后,当用户切换到其他应用程序或锁定iOS设备时,应用程序的音频可以继续。在iOS中支持高级回放功能,如AirPlay流媒体播放和图片中的图片回放,也需要此功能。

配置这些功能的最简单方法是使用Xcode。在Xcode中选择应用程序的目标并选择capability选项卡。在capability选项卡下,将背景模式设置为ON,并从可用模式列表中选择“Audio, AirPlay, and Picture in Picture”选项。

开启后台音频模式

启用此后台模式,并将音频会话配置为适当的类别,您的应用程序就可以开始播放后台音频了

注意:要允许视频演示的音频部分在后台播放,请参阅媒体播放编程指南中的播放背景音频

3. 激活音频会话

您已经通过设置音频会话的类别、选项和模式来配置它。要使配置生效,现在需要激活音频会话。

3.1 系统如何解决竞争的音频需求

当你的应用程序启动时,内置的应用程序(消息、音乐、Safari、手机)可能在后台运行。每一个都可以产生音频:一条短信来了,你10分钟前开始的播客还在继续播放,等等。

如果你把一个设备想象成一个机场,应用程序就像滑行的飞机,这个系统就像一个控制塔。你的应用程序可以发出音频请求并声明它想要的优先级,但是“在停机坪上”发生的事情的最终权威来自系统。使用音频会话与“控制塔”通信。图2-1展示了一个典型的场景—您的应用程序要求在音乐应用程序已经在播放时使用音频。在这个场景中,您的应用程序中断了音乐应用程序。

系统如何解决竞争的音频需求

在图的第1步中,应用程序请求激活它的音频会话。例如,在应用程序启动时,或者在用户轻击音频录制和回放应用程序中的Play按钮时,您可能会发出这样的请求。在步骤2中,系统将考虑激活请求。具体来说,它会考虑您分配给音频会话的类别。在图2-1中,您的应用程序使用了一个类别,该类别需要对其他音频进行消音。

在步骤3和步骤4中,系统关闭音乐应用程序的音频会话,停止其音频播放。最后,在第五步,系统激活你的应用程序的音频会话和播放可以开始。

3.2 激活和关闭您的音频会话

虽然AVFoundation回放和录制类会自动激活音频会话,但手动激活它会让您有机会测试激活是否成功。但是,如果你的应用程序有一个play/pause UI元素,那么编写你的代码,这样用户必须在激活会话之前按play。同样,在更改音频会话的活动/非活动状态时,请检查以确保调用成功。编写代码来优雅地处理系统拒绝激活您的会话。

该系统将关闭您的音频会话,用于时钟或日历闹钟或来电。当用户解除警报或选择忽略电话呼叫时,系统允许您的会话再次激活。是否在中断结束时重新激活会话取决于应用程序类型,如Audio Guidelines By app type所述。

  • 激活音频会话的代码如下:
let session = AVAudioSession.sharedInstance()
do {
    // 1) Configure your audio session category, options, and mode
    // 2) Activate your audio session to enable your custom configuration
    try session.setActive(true)
} catch let error as NSError {
    print("Unable to activate audio session:  \(error.localizedDescription)")
}

若要停用音频会话,请将false传递给setActive方法。

当使用AVFoundation对象(AVPlayer、AVAudioRecorder等)播放或录制音频时,系统负责在中断结束时重新激活音频会话。但是,如果您注册了通知消息并显式地重新激活音频会话,则可以验证重新激活成功,并可以更新应用程序的状态和用户界面。有关更多信息,请参见图3-1。

许多应用程序从来不需要显式地停用它们的音频会话。重要的例外包括VoIP应用程序、逐向导航应用程序,在某些情况下还包括回放和录音应用程序。

  • 确保VoIP应用程序的音频会话(通常在后台运行)仅在应用程序处理调用时是活动的。在后台,准备接收电话时,VoIP应用程序的音频会话不应处于激活状态。
  • 确保使用录制类别的应用程序的音频会话仅在录制时处于活动状态。在开始录制和停止录制之前,请确保您的会话处于非活动状态,以允许播放其他声音,例如传入消息警报。
  • 如果应用程序支持后台音频播放或录制,如果应用程序不积极使用音频(或准备使用音频),则在进入后台时禁用其音频会话。这样做允许系统释放音频资源,以便其他进程可以使用它们。它还可以防止应用程序的音频会话在应用程序进程被操作系统挂起时失效(参见AVAudioSessionInterruptionWasSuspendedKey)。

这个userInfo键只出现在AVAudioSession.InterruptionType中。开始中断事件,其中中断是操作系统挂起应用程序的直接结果。它的关联值是一个布尔NSNumber,其中一个真值表示中断是由于系统挂起应用程序造成的,而不是由另一个音频会话中断的。

3.2.1 Audio Guidelines By app type

3.3 检查其他音频是否正在播放

当你的应用程序激活时,声音可能已经在设备上播放了。例如,当用户启动应用程序时,音乐应用程序可能正在播放歌曲,或者Safari可能正在播放音频流。如果你的应用是一款游戏,了解是否有其他音频在播放尤其重要。许多游戏都有音乐音轨和音效。在iOS人机界面指南中,建议你假设用户在玩游戏时希望其他音频和游戏音效能够继续。

在你的应用委派的applicationDidBecomeActive:方法中,检查音频会话的secondaryAudioShouldBeSilencedHint属性,以确定音频是否已经在播放。当另一个具有非混合音频会话的应用程序正在播放音频时,该值为true。应用程序应该使用这个属性作为使次要于应用程序功能的音频静音的提示。例如,一个使用AVAudioSessionCategoryAmbient的游戏可以使用这个属性来决定是否应该在保持声音效果不静音的情况下静音音轨。

您还可以订阅类型为AVAudioSessionSilenceSecondaryAudioHintNotification的通知,以确保在可选的辅助音频静音应该开始或结束时通知您的应用程序。此通知仅发送给当前处于前台且具有活动音频会话的已注册侦听器。

func setupNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(handleSecondaryAudio),
                                           name: .AVAudioSessionSilenceSecondaryAudioHint,
                                           object: AVAudioSession.sharedInstance())
}
 
func handleSecondaryAudio(notification: Notification) {
    // Determine hint type
    guard let userInfo = notification.userInfo,
        let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt,
        let type = AVAudioSessionSilenceSecondaryAudioHintType(rawValue: typeValue) else {
            return
    }
 
    if type == .begin {
        // Other app audio started playing - mute secondary audio
    } else {
        // Other app audio stopped playing - restart secondary audio
    }
}

此通知的userInfo字典包含AVAudioSessionSilenceSecondaryAudioHintType值的AVAudioSessionSilenceSecondaryAudioHintTypeKey。使用音频提示类型来确定辅助音频静音应该开始还是结束

4. 响应中断

添加音频会话代码来处理中断,确保您的应用程序的音频在电话到来、时钟或日历闹铃响或其他应用程序激活其音频会话时继续正常工作。

音频中断是应用程序的音频会话的停用,它会立即停止音频。当来自应用程序的竞争音频会话被激活,并且该会话没有被系统分类以与您的会话混合时,就会发生中断。在您的会话变为非活动状态后,系统将发送一条“您被中断了”消息,您可以通过保存状态、更新用户界面等方式对其进行响应。

您的应用程序可能会在中断后暂停。当用户接电话时就会发生这种情况。如果用户忽略了一个呼叫,或者取消了一个警报,系统就会发出一个“中断结束”的消息,你的应用程序就会继续运行。要恢复您的音频,您必须重新激活您的音频会话。

4.1 中断生命周期

图3-1说明了播放应用程序的音频会话中断之前、期间和之后的事件序列。

中断生命周期

中断事件—在本例中,FaceTime请求的到来—按如下方式进行。编号的步骤与图中的数字相对应。

  1. 你的应用程序是活动的,播放音频。
  2. FaceTime请求到达。系统激活FaceTime应用程序的音频会话。
  3. 系统将关闭您的音频会话。此时,应用程序中的回放已经停止。
  4. 系统会发出一个通知,指示您的会话已被停用。
  5. 您的通知处理程序将采取适当的操作。例如,它可以更新用户界面并保存在停止回放时恢复回放所需的信息。
  6. 如果用户拒绝中断(忽略传入的FaceTime请求),系统就会发出通知,表明中断已经结束。
  7. 通知处理程序在中断结束时采取适当的操作。例如,它可以更新用户界面、重新激活音频会话和恢复播放。
  8. (图中未显示)如果用户接了一个电话,而不是在第6步取消中断,你的应用程序将被挂起。

4.2 音频中断处理技术

通过注册观察AVAudioSession发布的中断通知来处理中断。您在中断代码中所做的事情取决于您正在使用的音频技术,以及您在回放、录制、音频格式转换、读取流音频包等方面所使用的技术。一般来说,从用户的角度来看,您需要确保尽可能少的中断和尽可能好的恢复。

表3-1总结了中断期间适当的音频会话行为。如果您使用AVFoundation回放或录制对象,系统会自动处理其中的一些步骤。

表3-1音频会话中断期间应该发生什么:

时间 处理
中断开始后 1.保存状态和上下文,2.更新的用户界面
中断结束后 1.恢复状态和上下文,2.更新的用户界面,3.重新激活音频会话,如果适当的应用程序

表3-2总结了如何根据技术处理音频中断:

音频技术 中断处理
1. AVFoundation framework 系统自动暂停播放或录制中断和重新激活您的音频会话时,您恢复播放或录制。2.如果你想保存和恢复播放位置之间的应用程序启动,保存播放位置在中断以及在应用程序退出。
Audio Queue Services, I/O audio unit 这些技术让你的应用程序控制处理中断。你负责保存回放或录制位置,并在中断结束后重新激活音频会话。
System Sound Services 当中断开始时,使用系统声音服务播放的声音将变为静音。如果中断结束,声音可以再次播放。应用程序不能影响使用这种回放技术的声音的中断行为。

4.3 处理Siri中断

  • 当Siri打断你的应用程序回放时,你必须在音频会话处于中断状态时跟踪Siri发出的任何远程控制命令。在中断期间,跟踪Siri发出的所有命令,并在中断结束时做出相应的响应。例如,在中断期间,用户要求Siri暂停应用程序的音频播放。当你的应用程序被通知中断已经结束,它不应该自动恢复播放。相反,应用程序的UI应该指示应用程序处于暂停状态。

4.4 观察音频中断

  • 要处理音频中断,首先注册AVAudioSessionInterruptionNotification类型的通知。
func registerForNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(handleInterruption),
                                           name: .AVAudioSessionInterruption,
                                           object: AVAudioSession.sharedInstance())
}
 
func handleInterruption(_ notification: Notification) {
    // Handle interruption
}

  • 发布的NSNotification实例包含一个已填充的userInfo字典,提供中断的详细信息。通过从userInfo字典中检索AVAudioSessionInterruptionType值来确定中断的类型。中断类型指示中断是已经开始还是已经结束。
func handleInterruption(_ notification: Notification) {
    guard let info = notification.userInfo,
        let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
        let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
            return
    }
    if type == .began {
        // Interruption began, take appropriate actions (save state, update user interface)
    }
    else if type == .ended {
        guard let optionsValue =
            userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
                return
        }
        let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
        if options.contains(.shouldResume) {
            // Interruption Ended - playback should resume
        }
    }
}
  • 如果中断类型是AVAudioSessionInterruptionTypeEnded,则userInfo字典可能包含AVAudioSessionInterruptionOptions值。AVAudioSessionInterruptionOptionShouldResume的一个选项值是一个提示,指示如果应用程序在被中断时正在播放,它是否应该自动恢复播放。媒体播放应用程序在中断后开始播放之前应始终寻找此标志。如果它不存在,播放不应该再次开始,直到由用户发起。没有播放界面的应用程序,比如游戏,可以忽略这个标志,在中断结束时重新激活并恢复播放。

注意:不能保证开始中断会有相应的结束中断。你的应用程序需要知道一个切换到前台运行状态或用户按下播放按钮。在这两种情况下,确定你的应用程序是否应该重新激活它的音频会话。

4.5 响应媒体服务器重置

媒体服务器通过共享服务器进程提供音频和其他多媒体功能。虽然很少见,但是媒体服务器可以在您的应用程序处于活动状态时进行重置。注册AVAudioSessionMediaServicesWereResetNotification通知以监视媒体服务器重置。收到通知后,您的app需要做以下工作:

  • 处理孤立的音频对象(如播放器、录音机、转换器或音频队列)并创建新对象
  • 重置任何被跟踪的内部音频状态,包括AVAudioSession的所有属性
  • 在适当的时候,使用setActive:error:方法重新激活AVAudioSession实例

5. 响应路由更改

当您的应用程序运行时,用户可能会插入或拔出耳机,或使用带有音频连接的坞站。iOS人机界面指南描述了应用程序应该如何应对此类事件。要实现这些建议,请编写音频会话代码来处理音频硬件路由更改。某些类型的应用程序,比如游戏,并不总是需要对路线变化做出响应。然而,其他类型的应用程序,如媒体播放器,必须响应所有的路由变化。

5.1 各种音频硬件路由变化

音频硬件路由是音频信号的有线电子路径。当设备的用户插入或拔出耳机时,系统会自动改变音频硬件路线。如果你注册了AVAudioSessionRouteChangeNotification类型的通知,你的应用程序可以被通知这些变化。

图4-1描述了在录制和回放期间各种路由更改的事件序列。图的底部显示了四个可能的结果,它们来自您编写的属性侦听器回调函数所采取的操作。

处理硬件线路变换

如图所示,在您的app启动后,系统最初会确定音频路由。它将在应用程序运行时继续监视活动路由。首先考虑用户点击应用程序中的记录按钮的情况,该按钮由图左侧的“开始记录”框表示。

在录制过程中,用户可以将耳机插入或拔出,如图左下方所示为钻石形状的决策元件。作为响应,系统发送一个路由更改通知,其中包含更改的原因和前一个路由。你的应用程序应该停止录制。

回放的情况类似,但结果不同,如图右侧所示。如果用户在播放过程中拔下耳机,应用程序应该暂停音频。如果用户在回放过程中插入耳机,应用程序应该允许回放继续。

5.2 观察音频路由的变化

  • 发生音频路由更改的原因有很多,包括用户插入一对耳机、连接蓝牙LE耳机或拔掉USB音频接口。知道什么时候发生这些变化可能对你的应用程序很重要,这样你就可以更新它的用户界面或改变它的内部状态。通过注册AVAudioSessionRouteChangeNotification类型的通知,可以在音频路由更改时通知您。
func setupNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(handleRouteChange),
                                           name: .AVAudioSessionRouteChange,
                                           object: AVAudioSession.sharedInstance())
}
 
func handleRouteChange(_ notification: Notification) {
 
}
  • 发布的通知包含一个已填充的userInfo字典,提供路由更改的详细信息。您可以通过从userInfo字典中检索AVAudioSessionRouteChangeReason值来确定这种变化的原因。当连接一个新设备时,原因是AVAudioSessionRouteChangeReasonNewDeviceAvailable,当一个设备被删除时,原因是avaudiosessionroutechangereasonolddeviceavailable。
func handleRouteChange(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
        let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
            return
    }
    switch reason {
    case .newDeviceAvailable:
        // Handle new device available.
    case .oldDeviceUnavailable:
        // Handle old device removed.
    default: ()
    }
}
  • 当新设备可用时,您可以查询音频会话的currentRoute属性,以确定音频输出当前路由到何处。这将返回一个AVAudioSessionRouteDescription对象,该对象列出了音频会话的所有输入和输出。当设备被移除时,您可以从userInfo字典中检索上一条路由的AVAudioSessionRouteDescription对象。在这两种情况下,您可以查询路由描述的输出属性,该属性返回一个AVAudioSessionPortDescription对象数组,提供音频输出路由的详细信息。
func handleRouteChange(notification: NSNotification) {
    guard let userInfo = notification.userInfo,
        let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
            return
    }
    switch reason {
    case .newDeviceAvailable:
        let session = AVAudioSession.sharedInstance()
        for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
            headphonesConnected = true
        }
    case .oldDeviceUnavailable:
        if let previousRoute =
            userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
            for output in previousRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
                headphonesConnected = false
            }
        }
    default: ()
    }
}

重要提示:媒体播放应用程序应该暂停播放,如果路线改变的原因是AVAudioSessionRouteChangeReasonOldDeviceUnavailable,但不应该,如果原因是AVAudioSessionRouteChangeReasonOverride。

注意:音频路由的更改也可能导致音频会话的采样率、I/O缓冲区持续时间、通道计数或其他硬件相关值的更改。如果这些值对您的应用程序很重要,请在路由更改后查询它们,以查看它们的值是否已更改。

6. 配置设备硬件

使用音频会话属性,您可以在运行时为设备硬件优化应用程序的音频行为。这样做可以使您的代码适应它所运行的设备的特性,以及用户在应用程序运行时所做的更改(如插入耳机或将设备停靠在其上)。

使用AVAudioSession:

  • 为示例速率和I/O缓冲区持续时间指定首选硬件设置
  • 查询许多硬件特性,如输入和输出延迟、输入和输出通道计数、硬件采样率、硬件卷设置和音频输入的可用性

6.1 选择首选音频硬件值

使用音频会话指定您的首选设备设置,如采样率和硬件I/O缓冲区持续时间。表5-1描述了这些偏好的好处和代价。


选择硬件

例如,如果音频质量在您的应用程序中非常重要,并且大文件或缓冲区大小不是一个重要问题,那么您可以指定一个高采样率的首选项。

注意:默认的音频I/O缓冲时间(44.1 kHz音频大约0.02秒)为大多数应用程序提供了足够的响应能力。您可以为延迟关键型应用程序(如live musical instrument monitoring)设置一个较低的I/O持续时间,但是对于大多数应用程序,您永远不需要修改这个设置。

6.2 设置首选音频硬件值

在激活音频会话之前设置首选硬件值。如果你已经在运行一个音频会话,那么就停用它。对首选值的更改将在音频会话激活后生效,您可以在此时验证更改。清单5-1展示了如何设置首选硬件值以及如何验证它们。

  • 清单5-1设置和验证音频硬件值
let session = AVAudioSession.sharedInstance()
 
// Configure category and mode
do {
    try session.setCategory(AVAudioSessionCategoryRecord, mode: AVAudioSessionModeDefault)
} catch let error as NSError {
    print("Unable to set category:  \(error.localizedDescription)")
}
 
// Set preferred sample rate
do {
    try session.setPreferredSampleRate(44_100)
} catch let error as NSError {
    print("Unable to set preferred sample rate:  \(error.localizedDescription)")
}
 
// Set preferred I/O buffer duration
do {
    try session.setPreferredIOBufferDuration(0.005)
} catch let error as NSError {
    print("Unable to set preferred I/O buffer duration:  \(error.localizedDescription)")
}
 
// Activate the audio session
do {
    try session.setActive(true)
} catch let error as NSError {
    print("Unable to activate session. \(error.localizedDescription)")
}
 
// Query the audio session's ioBufferDuration and sampleRate properties
// to determine if the preferred values were set
print("Audio Session ioBufferDuration: \(session.ioBufferDuration), sampleRate: \(session.sampleRate)")

注意:当竞争音频会话设置首选硬件值时,系统优先考虑非混合会话。使用AVAudioSessionCategoryAmbient类别或AVAudioSessionCategoryOptionMixWithOthers选项的音频会话不太可能提供其首选的硬件设置。

6.3 选择和配置麦克风

在拥有两个或更多内置麦克风的设备上,iOS会通过音频会话模式自动选择麦克风。模式指定用于输入的数字信号处理(DSP)和可能的路由。为每种模式的用例优化输入和路由。设置模式还可能影响正在使用的路由的其他方面。

开发人员还可以手动选择麦克风,如果硬件支持,甚至可以选择首选的麦克风极性模式。

重要提示:在使用任何输入选择功能之前,为您的应用程序设置音频会话类别和模式,然后激活音频会话。

  • 设置首选输入

要发现内置的或连接的输入端口,请使用音频会话的availableInputs属性。此属性返回AVAudioSessionPortDescription对象数组,这些对象描述设备的可用输入端口。端口可以通过它们的portType属性来标识。要设置首选输入端口(内置麦克风、有线麦克风、USB输入,等等),请使用音频会话的setPreferredInput:error:方法。

  • 设置首选数据源
  1. 一些端口,如内置麦克风和一些USB配件,支持数据源。应用程序可以通过查询端口描述的dataSources属性来发现可用的数据源。对于内置麦克风,返回的数据源描述对象表示每个麦克风。不同的设备为内置麦克风返回不同的值。例如,iPhone 4和iPhone 4S有两个麦克风:底部和顶部。iPhone 5有三个麦克风:底部、前面和后面。
  2. 单个内置麦克风可以通过数据源描述的位置属性(上、下)和方向属性(前、后等)的组合进行标识。应用程序可以使用AVAudioSessionPortDescription对象的setPreferredDataSource:error:方法设置首选数据源。
  • 设置一个首选的极坐标模式

一些iOS设备支持为一些内置麦克风配置麦克风极性模式。麦克风的极模式决定了它对声音的敏感度与声源的方向有关。当前的iphone支持为前后内置麦克风设置首选的极坐标模式。可用模式使用数据源描述对象的supportedPolarPatterns属性返回。此属性为数据源返回一个受支持的极性模式数组(如cardioid或omnidirectional),如果没有可用的可选模式,则返回nil。如果数据源有许多受支持的极性模式,您可以通过使用数据源描述的setPreferredPolarPattern:error:方法来设置首选的极性模式。

  • 下面的代码提供了一个简单的示例,演示如何选择特定的麦克风并设置其极性模式。
// Preferred Mic = Front, Preferred Polar Pattern = Cardioid
let preferredMicOrientation = AVAudioSessionOrientationFront
let preferredPolarPattern = AVAudioSessionPolarPatternCardioid
 
// Retrieve your configured and activated audio session
let session = AVAudioSession.sharedInstance()
 
// Get available inputs
guard let inputs = session.availableInputs else { return }
 
// Find built-in mic
guard let builtInMic = inputs.first(where: {
    $0.portType == AVAudioSessionPortBuiltInMic
}) else { return }
 
// Find the data source at the specified orientation
guard let dataSource = builtInMic.dataSources?.first (where: {
    $0.orientation == preferredMicOrientation
}) else { return }
 
// Set data source's polar pattern
do {
    try dataSource.setPreferredPolarPattern(preferredPolarPattern)
} catch let error as NSError {
    print("Unable to preferred polar pattern: \(error.localizedDescription)")
}
 
// Set the data source as the input's preferred data source
do {
    try builtInMic.setPreferredDataSource(dataSource)
} catch let error as NSError {
    print("Unable to preferred dataSource: \(error.localizedDescription)")
}
 
// Set the built-in mic as the preferred input
// This call will be a no-op if already selected
do {
    try session.setPreferredInput(builtInMic)
} catch let error as NSError {
    print("Unable to preferred input: \(error.localizedDescription)")
}
 
// Print Active Configuration
session.currentRoute.inputs.forEach { portDesc in
    print("Port: \(portDesc.portType)")
    if let ds = portDesc.selectedDataSource {
        print("Name: \(ds.dataSourceName)")
        print("Polar Pattern: \(ds.selectedPolarPattern ?? "[none]")")
    }
}

在iPhone 6s上运行这段代码会产生以下控制台输出:

Port: MicrophoneBuiltIn
Name: Front
Polar Pattern: Cardioid

6.4 在模拟器中运行应用程序

当你添加音频会话支持到你的应用程序时,你可以在模拟器或设备上运行你的应用程序。但是,模拟器不能模拟不同进程或音频路由更改中的音频会话之间的大多数交互。当运行你的应用程序在模拟器,你不能:

  • 调用一个中断
  • 模拟插入或拔出耳机
  • 改变静音开关的设置
  • 模拟屏幕锁
  • 测试音频混合行为——即将音频与来自另一个应用程序(如音乐应用程序)的音频一起播放。

由于模拟器的特性,您可能希望对代码进行条件设置,以允许在模拟器中进行部分测试。下面代码展示了如何做到这一点。

#if arch(i386) || arch(x86_64)
    // Execute subset of code that works in the Simulator
#else
    // Execute device-only code as well as the other code
#endif

7. 保护用户隐私

为了保护用户隐私,你的应用程序在录制音频之前必须获得用户的许可。如果用户不授予权限,则只记录静默。当你使用一个支持记录的类别,而应用程序试图使用输入路径时,系统会自动提示用户允许。

您可以使用requestRecordPermission:方法来手动请求权限,而不是等待系统提示用户记录权限。使用这种方法允许您的应用程序在不中断应用程序的自然流的情况下获得许可,从而获得更好的用户体验。

AVAudioSession.sharedInstance().requestRecordPermission { granted in
    if granted {
        // User granted access. Present recording interface.
    } else {
        // Present message to user indicating that recording
        // can't be performed until they change their preference
        // under Settings -> Privacy -> Microphone
    }
}

重要提示:从iOS 10开始,所有访问设备麦克风的应用程序必须静态地声明它们的意图。要做到这一点,应用程序现在必须在它们的信息中包含NSMicrophoneUsageDescription键。并为此键提供一个目的字符串。当系统提示用户允许访问时,此字符串将作为警报的一部分显示。
如果一个应用程序试图访问设备的麦克风没有这个键和值存在,应用程序将终止。

8. AVAudioSession

AVAudioSession是一个对象,它向系统传达你打算如何在你的应用程序中使用音频。它的类声明如下:

class AVAudioSession : NSObject

音频会话充当应用程序和操作系统之间的中介,进而充当底层音频硬件之间的中介。您使用一个音频会话来与操作系统通信应用程序音频的一般性质,而不详细说明特定的行为或与音频硬件所需的交互。您将这些细节的管理委托给音频会话,以确保操作系统能够最好地管理用户的音频体验。

所有的iOS、tvOS和watchOS应用程序都有一个默认的音频会话,并预先配置了以下行为:

  • 它支持音频回放,但不允许音频录制(tvOS不支持音频录制)。
  • 在iOS系统中,将铃声/静音开关设置为静音模式,应用程序播放的任何音频都会被静音。
  • 在iOS系统中,锁定设备会使应用程序的音频静音。
  • 当应用程序播放音频时,它会静音任何其他背景音频。

虽然默认的音频会话提供了有用的行为,但它通常不提供媒体应用程序需要的音频行为。要更改默认行为,需要配置应用程序的音频会话类别。

你可以使用七种可能的类别(参见音频会话类别和模式),但是回放是回放应用程序最常用的一种。这个类别表明音频播放是你的应用程序的核心功能。当你指定这个类别时,你的应用程序的音频将继续与铃声/静音开关设置为静音模式(只有iOS)。使用这个类别,你也可以播放背景音频,如果你在图片背景模式中使用音频,AirPlay和图片。有关更多信息,请参见启用背景音频。

使用AVAudioSession对象来配置应用程序的音频会话。该类是一个单例对象,用于设置音频会话的类别、模式和其他配置。您可以在应用程序的整个生命周期中与音频会话进行交互,但是在应用程序启动时执行此配置通常很有用,如下面的示例所示。

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    // Get the singleton instance.
    let audioSession = AVAudioSession.sharedInstance()
    do {
        // Set the audio session category, mode, and options.
        try audioSession.setCategory(.playback, mode: .moviePlayback, options: [])
    } catch {
        print("Failed to set audio session category.")
    }
    
    // Other post-launch configuration.
    return true
}

当您使用setActive(:)或setActive(:options:)方法激活会话时,音频会话将使用此配置。

请注意
你可以在设置类别后的任何时候激活音频会话,但通常最好将此调用推迟到应用程序开始音频播放之后。延迟调用可以确保您不会过早地中断正在进行的任何其他背景音频。

8.1 获取音频会话

class func sharedInstance() -> AVAudioSession
//Returns the shared audio session instance.

8.2 配置音频会话


var category: AVAudioSession.Category
//The current audio session category.
func setCategory(AVAudioSession.Category)
//Sets the audio session’s category.
var availableCategories: [AVAudioSession.Category]
//The audio session categories available on the current device.
struct AVAudioSession.Category
//Audio session category identifiers.
var categoryOptions: AVAudioSession.CategoryOptions
//The set of options associated with the current audio session category.
func setCategory(AVAudioSession.Category, options: AVAudioSession.CategoryOptions)
//Sets the audio session’s category with the specified options.
struct AVAudioSession.CategoryOptions
//Constants that specify optional audio behaviors.
var mode: AVAudioSession.Mode
//The current audio session’s mode.
func setMode(AVAudioSession.Mode)
//Sets the audio session’s mode.
func setCategory(AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions)
//Sets the audio session’s category, mode, and options.
var availableModes: [AVAudioSession.Mode]
//The audio session modes available on the device.
struct AVAudioSession.Mode
//Audio session mode identifiers.
var routeSharingPolicy: AVAudioSession.RouteSharingPolicy
//The current route-sharing policy.
func setCategory(AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions)
//Sets the session category, mode, route-sharing policy, and options.
enum AVAudioSession.RouteSharingPolicy
//Cases that indicate the possible route-sharing policies for an audio session.

8.3 激活音频会话

//使用指定的选项激活或关闭应用程序的音频会话。
func setActive(Bool, options: AVAudioSession.SetActiveOptions)

//在watchOS上异步激活音频会话。
func activate(options: AVAudioSessionActivationOptions, completionHandler: (Bool, Error?) -> Void)

8.4 请求录音权限

//当前的录音许可状态。
var recordPermission: AVAudioSession.RecordPermission

//请求用户录制音频的权限。
func requestRecordPermission(PermissionBlock)

8.5 与其他音频混合


var isOtherAudioPlaying: Bool
//A Boolean value that indicates whether another app is playing audio.
var secondaryAudioShouldBeSilencedHint: Bool
//A Boolean value that indicates whether another app, with a nonmixable audio session, is playing audio.
var allowHapticsAndSystemSoundsDuringRecording: Bool
//A Boolean value that indicates whether system sounds and haptics play while recording from audio input.
func setAllowHapticsAndSystemSoundsDuringRecording(Bool)
//Sets a Boolean value that indicates whether system sounds and haptics play while recording from audio input.
var promptStyle: AVAudioSession.PromptStyle
//A hint to audio sessions that use voice prompt mode to alter the type of prompts they issue in response to other system audio, such as Siri and phone calls.
enum AVAudioSession.PromptStyle
//Constants that indicate the prompt style to use.

8.6 响应音频会话通知

class let interruptionNotification: NSNotification.Name
//A notification that’s posted when an audio interruption occurs.
class let routeChangeNotification: NSNotification.Name
//A notification that’s posted when the system’s audio route changes.
class let silenceSecondaryAudioHintNotification: NSNotification.Name
//A notification that’s posted when the primary audio from other applications starts and stops.
class let mediaServicesWereLostNotification: NSNotification.Name
//A notification that’s posted when the system terminates the media server.
class let mediaServicesWereResetNotification: NSNotification.Name
//A notification that’s posted when the media server restarts.

8.7 使用音频路由

//当前音频路由的输入和输出端口的描述。
var currentRoute: AVAudioSessionRouteDescription

//一个对象,它描述与会话的音频路由相关联的输入和输出端口。
class AVAudioSessionRouteDescription

//一个布尔值,指示音频输入路径是否可用。
var isInputAvailable: Bool

//一个可用于音频路由的输入端口数组。
var availableInputs: [AVAudioSessionPortDescription]?

//音频路由的首选输入端口。
var preferredInput: AVAudioSessionPortDescription?

//设置音频路由的首选输入端口。
func setPreferredInput(AVAudioSessionPortDescription?)

//关于端口功能及其支持的硬件通道的信息。
class AVAudioSessionPortDescription

//当前选择的输入数据源。
var inputDataSource: AVAudioSessionDataSourceDescription?

//音频会话当前输入端口的可用数据源数组。
var inputDataSources: [AVAudioSessionDataSourceDescription]?

//选择音频会话当前输入端口的数据源。
func setInputDataSource(AVAudioSessionDataSourceDescription?)

//当前音频路由的可用输出数据源的数组。
var outputDataSources: [AVAudioSessionDataSourceDescription]?

//当前选择的输出数据源。
var outputDataSource: AVAudioSessionDataSourceDescription?

//设置音频会话的输出数据源。
func setOutputDataSource(AVAudioSessionDataSourceDescription?)

//为音频输入或输出定义数据源的对象,提供数据源的名称、位置和方向等信息。
class AVAudioSessionDataSourceDescription

//临时改变当前的音频路由。
func overrideOutputAudioPort(AVAudioSession.PortOverride)

8.8 使用音频通道

//当前路由的音频输入通道的数量。
var inputNumberOfChannels: Int

//当前音频路由可用的最大输入通道数。
var maximumInputNumberOfChannels: Int

//当前路由的优先输入通道数。
var preferredInputNumberOfChannels: Int

//设置当前路由的优先输入通道数。
func setPreferredInputNumberOfChannels(Int)

//音频输出通道的数目。
var outputNumberOfChannels: Int

//当前音频路由可用的最大输出通道数。
var maximumOutputNumberOfChannels: Int

//当前路由的输出通道的优先数量。
var preferredOutputNumberOfChannels: Int

//设置当前路由的输出通道的首选数目。
func setPreferredOutputNumberOfChannels(Int)

8.9 处理音频设备设置



//应用于与会话相关的输入的增益。
var inputGain: Float

//一个布尔值,指示是否可以设置输入增益。
var isInputGainSettable: Bool

//将输入增益更改为指定值。
func setInputGain(Float)

//由用户设置的系统范围的输出卷。
var outputVolume: Float

//当前音频采样率,单位为赫兹。
var sampleRate: Double

//首选采样率,单位为赫兹。
var preferredSampleRate: Double

//设置音频输入和输出的首选采样率。
func setPreferredSampleRate(Double)

//音频输入的延迟,以秒为单位。
var inputLatency: TimeInterval

//音频输出的延迟,以秒为单位。
var outputLatency: TimeInterval

//当前I/O缓冲区持续时间(以秒为单位)。
var ioBufferDuration: TimeInterval

//首选的I/O缓冲区持续时间(以秒为单位)。
var preferredIOBufferDuration: TimeInterval

//设置首选音频I/O缓冲区持续时间。
func setPreferredIOBufferDuration(TimeInterval)

8.10 设置聚合的I/O首选项

//设置音频会话的聚合I/O配置首选项。
func setAggregatedIOPreference(AVAudioSession.IOType)

参考苹果官方文档:音频会话章节

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容