AVFoundation(二)音频播放与录制

AVFoundation(一)概览
AVFoundation(二)音频播放与录制

1. 音频会话AVAudioSession

iOS系统提供了一个可管理的音频环境(managed audio environment),可以带给所有iOS用户非常好的体验,这一神奇的过程就是通过音频会话(audio session)来实现的。

音频会话在应用程序和操作系统之间扮演者中间人的角色,我们可以指明应用程序的一般行为,并可以把对该行为的管理委托给音频会话,这样系统就可以对用户使用音频的体验进行最适当的管理。

所有iOS应用程序都具有音频会话,无论其是否使用。默认音频会话来自于以下一些预配置:

  • 激活了音频播放,但是音频录制未激活
  • 当用户切换响铃/静音开关到"静音"模式时,应用程序播放的所有音频都会消失
  • 当设备显示解锁屏幕时,应用程序的音频处于静音状态
  • 当应用程序播放音频时,所有后台播放的音频都会处于静音状态

默认音频会话提供了很多实用功能,我们也可以使用"分类"的功能,来很容易的定制我们的特殊需求。

1.1 音频会话分类

AVFoundation定义了7种分类来描述应用程序所使用的音频行为,如下图


音频会话分类
AVAudioSession.Category

可以看到,其实默认的就是“AVAudioSessionCategorySoloAmbient”类别。从表中我们可以总结如下:

  • AVAudioSessionCategoryAmbient : 只用于播放音乐时,并且可以和QQ音乐同时播放,比如玩游戏的时候还想听QQ音乐的歌,那么把游戏播放背景音就设置成这种类别。同时,当用户锁屏或者静音时也会随着静音,这种类别基本使用所有App的背景场景。
  • AVAudioSessionCategorySoloAmbient: 也是只用于播放,但是和AVAudioSessionCategoryAmbient不同的是,用了它就别想听QQ音乐了,比如不希望QQ音乐干扰的App,类似节奏大师。同样当用户锁屏或者静音时也会随着静音,锁屏了就玩不了节奏大师了。
  • AVAudioSessionCategoryPlayback: 如果锁屏了还想听声音怎么办?用这个类别,比如App本身就是播放器,同时当App播放时,其他类似QQ音乐就不能播放了。所以这种类别一般用于播放器类App
  • AVAudioSessionCategoryRecord: 有了播放器,肯定要录音机,比如微信语音的录制,就要用到这个类别,既然要安静的录音,肯定不希望有QQ音乐了,所以其他播放声音会中断。想想微信语音的场景,就知道什么时候用他了。
  • AVAudioSessionCategoryPlayAndRecord: 如果既想播放又想录制该用什么模式呢?比如VoIP,打电话这种场景,PlayAndRecord就是专门为这样的场景设计的 。
  • AVAudioSessionCategoryMultiRoute: 想象一个DJ用的App,手机连着HDMI到扬声器播放当前的音乐,然后耳机里面播放下一曲,这种常人不理解的场景,这个类别可以支持多个设备输入输出。
  • AVAudioSessionCategoryAudioProcessing: 主要用于音频格式处理,一般可以配合AudioUnit进行使用

上述分类提供的几种常见行为可以满足大部分应用程序的需要,不过如果开发者需要更复杂的功能,其中一些分类可以通过使用optionsmodes方法进一步定义开发,options可以让开发者使用一些附加行为,如使用Playback分类后,应用程序允许将输出音频和背景声音进行混合。modes可以通过引入被定制的行为进一步对分类进行修改以满足一些特殊需求。

1.2 配置音频会话

音频会话在应用程序的生命周期中是可以修改的,但通常我们只对其配置一次,就是在应用程序启动时,也就是在func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool方法中

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(.playback)
        } catch let error {
            print(error)
        }
        do {
            try session.setActive(true)
        } catch let error {
            print(error)
        }

        return true
    }

AVAudioSession提供了与应用程序音频会话交互的接口,所以开发者需要取得指向该单例的指针。通过设置合适的分类,开发者可为音频的播放指定需要的音频会话,在其中定制一些行为。最后告知该音频会话激活该配置。

2 使用AVAudioPlayer播放音频

2.1 创建AVAudioPlayer

AVAudioPlayer可以播放内存版本的Data,或者本地音频文件的URL,如果基于iOS系统,URL必须在应用程序沙盒之内或者该URL一定是用户iPod库中的一个元素。

class LWAudioViewController: BaseViewController {
    
    private var player: AVAudioPlayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let fileUR = Bundle.main.url(forResource: "音乐", withExtension: "mp3")
        guard let fileURL = fileUR else {
            return
        }
        player = try! AVAudioPlayer(contentsOf: fileURL)
        player.prepareToPlay()
    }
}

如上所示,我们最好使用prepareToPlay(),因为这个方法会取得需要的音频硬件并预加载Audio Queue的缓存区。如果直接使用payer(),它会隐形激活prepareToPlay()方法,但是我们会感觉到开始点击播放和实际播放之间会有一个延时。

2.2 AVAudioPlayer的属性与方法

  • volume:播放器音量,值在0.0(静音)到1.0(最大音量)之间
  • pan:立体音,取值范围-1.0到1.0,-1.0表示极左,0.0表示中间,1.0表示极右
  • rate:播放速度,取值范围0.5到2.0,0.5表示半速,2.0表示倍速,rate要起作用,必须设置enableRate为true
  • numberOfLoops:设置0表示单次不循环,设置大于0的数n,表示执行n次循环,设置-1表示无限循环
  • open func prepareToPlay() -> Bool:将音频预加载到Audio Queue缓存区
  • open func play() -> Bool:播放音频
  • open func play(atTime time: TimeInterval) -> Bool:延时播放音频
  • open func pause():暂停播放
  • open func stop():停止播放

2.3 配置后台播放

首先,在target->signing&Capabilities中,选中后台播放


配置后台播放

然后,在这个位置添加如下代码设置音频会话

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(.playback)
        } catch let error {
            print(error)
        }
        do {
            try session.setActive(true)
        } catch let error {
            print(error)
        }

        return true
    }

这样就设置后,在APP退入后台或者设备锁屏后,音频依然可以根据之前代码中的预设播放了

2.4 处理中断事件

中断事件在iOS设备中经常出现,如电话呼入、闹钟响起以及谈起FaceTime请求等,这时会出现音频播放中断,而且之前事件结束后音频也没有再次播放。我们可以如下操作:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(.playback)
        } catch let error {
            print(error)
        }
        do {
            try session.setActive(true)
        } catch let error {
            print(error)
        }
        //添加观察者
        NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        
        return true
    }

    //处理打断事件
    @objc private func handleInterreption(_ noti: Notification){
        guard let userInfo = noti.userInfo,
            userInfo.keys.contains(AVAudioSessionInterruptionTypeKey),
            let type  = userInfo[AVAudioSessionInterruptionTypeKey] as? AVAudioSession.InterruptionType else {
            return
        }
        switch type {
        case .began:
            /*其实到这里播放已经被中断了,我们切换显示的状态即可*/
            print("打断开始,暂停播放,切换状态")
        case .ended:
            /*
             当打断结束,通知中会返回一个InterruptionOptions来表明
             音频会话是否已经重新激活以及是否可以再次播放
             */
            if let option = userInfo[AVAudioSessionInterruptionOptionKey] as? AVAudioSession.InterruptionOptions,
                option == AVAudioSession.InterruptionOptions.shouldResume {
                print("打断结束,恢复播放,切换状态")
            }
            
        @unknown default:
            break
        }
    }

3.线路改变

在iOS设备上添加或移除音频输入、输出线路时,会发生线路改变。有多重原因可以导致,如用户插入耳机或断开USB麦克风。当这些事件发生时,音频会根据情况改变输入或输出线路,同时AVAudioSession会广播一个描述该变化的通知给所有相关的侦听器。我们也可以通过注册相关的通知来处理该事件:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(.playback)
        } catch let error {
            print(error)
        }
        do {
            try session.setActive(true)
        } catch let error {
            print(error)
        }
        //添加观察者,观察打断事件
        NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        //添加观察者,观察线路改变事件
        NotificationCenter.default.addObserver(self, selector: #selector(hanleRouteChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
        return true
    }
    //处理音频
    @objc private func hanleRouteChange(_ noti: Notification){
        guard let userInfo = noti.userInfo,
            userInfo.keys.contains(AVAudioSessionRouteChangeReasonKey),
            let reason  = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason else {
                return
        }
        switch reason {
        case .unknown :
            print("未知原因")
        case .oldDeviceUnavailable:
            print("旧设备不可用,如拔掉耳机事件")
            if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription,let previousOutput = previousRoute.outputs.first {
                let portType = previousOutput.portType
                if portType == .headphones {
                    //headphones是有线耳机
                    print("停止播放")
                }
            }
        case .newDeviceAvailable:
            print("有可用的新设备,如插入耳机")
        case .categoryChange:
            break
        case .override:
            break
        case .wakeFromSleep:
            break
        case .noSuitableRouteForCategory:
            break
        case .routeConfigurationChange:
            print("设备配置改变")
        @unknown default:
            break
        }
    }

4. 使用AVAudioRecorder录制音频

4.1 创建AVAudioRecorder

AVAudioRecorder也是构建于Audio Queue Service之上的,我们创建AVAudioRecorder实例时需要为其提供数据的一些信息,分别是:

  • 用于表示音频流写入文件的本地文件URL
  • 包含用于配置录音会话键值信息的字典对象
private func recordSound(){
  let filePath = kPathTemp + "sound.m4a"
  let url = URL(fileURLWithPath: filePath)
  let setting = [AVFormatIDKey: kAudioFormatMPEG4AAC,AVSampleRateKey:22050.0,AVNumberOfChannelsKey:1] as [String : Any]
  self.recorder = try! AVAudioRecorder(url: url, settings: setting)
  self.recorder.prepareToRecord()
}

和AVPlayer的prepareToPlay方法类似,这个方法执行底层Audio Queue初始化的必要过程。该方法还在URL参数指定的位置创建一个文件,将录制启动时的延时降到最小。
字典设置的key都定义在AVFoundation->AVFAudio->AVAudioSettings文件中,包含了音频格式、采样率、通道数、指定格式

4.1.1 音频格式

AVFormatIDKey定义了写入内容的音频格式,下面的常量都是音频格式所支持的值:

CF_ENUM(AudioFormatID)
{
    kAudioFormatLinearPCM               = 'lpcm',
    kAudioFormatAC3                     = 'ac-3',
    kAudioFormat60958AC3                = 'cac3',
    kAudioFormatAppleIMA4               = 'ima4',
    kAudioFormatMPEG4AAC                = 'aac ',
    kAudioFormatMPEG4CELP               = 'celp',
    kAudioFormatMPEG4HVXC               = 'hvxc',
    kAudioFormatMPEG4TwinVQ             = 'twvq',
    kAudioFormatMACE3                   = 'MAC3',
    kAudioFormatMACE6                   = 'MAC6',
    kAudioFormatULaw                    = 'ulaw',
    kAudioFormatALaw                    = 'alaw',
    kAudioFormatQDesign                 = 'QDMC',
    kAudioFormatQDesign2                = 'QDM2',
    kAudioFormatQUALCOMM                = 'Qclp',
    kAudioFormatMPEGLayer1              = '.mp1',
    kAudioFormatMPEGLayer2              = '.mp2',
    kAudioFormatMPEGLayer3              = '.mp3',
    kAudioFormatTimeCode                = 'time',
    kAudioFormatMIDIStream              = 'midi',
    kAudioFormatParameterValueStream    = 'apvs',
    kAudioFormatAppleLossless           = 'alac',
    kAudioFormatMPEG4AAC_HE             = 'aach',
    kAudioFormatMPEG4AAC_LD             = 'aacl',
    kAudioFormatMPEG4AAC_ELD            = 'aace',
    kAudioFormatMPEG4AAC_ELD_SBR        = 'aacf',
    kAudioFormatMPEG4AAC_ELD_V2         = 'aacg',    
    kAudioFormatMPEG4AAC_HE_V2          = 'aacp',
    kAudioFormatMPEG4AAC_Spatial        = 'aacs',
    kAudioFormatAMR                     = 'samr',
    kAudioFormatAMR_WB                  = 'sawb',
    kAudioFormatAudible                 = 'AUDB',
    kAudioFormatiLBC                    = 'ilbc',
    kAudioFormatDVIIntelIMA             = 0x6D730011,
    kAudioFormatMicrosoftGSM            = 0x6D730031,
    kAudioFormatAES3                    = 'aes3',
    kAudioFormatEnhancedAC3             = 'ec-3'
};

指定kAudioFormatLinearPCM会将未压缩的音频流写入到文件中。这种格式的保真度最高,不过相应的文件也最大。选择AAC(kAudioFormatMPEG4AAC)AppleIMA4(kAudioFormatAppleIMA4)的压缩格式会显著缩小文件,还能保证高质量的音频内容。

注意:
你所指定的音频格式一定要和URL参数定义的文件类型兼容。比如,如果录制一个名为test.wav的文件,隐含的意思就是录制的音频必须满足Waveform Audio File Format(WAVE)的格式要求,即低字节序、Linear PCM。为AVFormatIDKey值指定除kAudioFormatLinearPCM之外的值会导致错误。

4.1.2 采样率

AVSampleRateKey用于定义录音器的采样率。采样率定义了对输入的模拟音频信号每一秒的采样数。在录制音频的质量及最终文件大小方面,采样率扮演者至关重要的角色。使用低采样率,比如8kHz,会导致粗粒度、AM广播类型的录制效果,不过文件会比较小;使用44.1kHz的采样率(CD质量的采样率)会得到非常高质量的内容,不过文件就比较大。对于使用什么采样率最好没有一个明确的定义,不过开发者应该尽量使用标准的采样率,比如8kHz、16kHz、22050Hz、44100Hz

4.1.3 通道数

AVNumberOfChannelsKey用于定义记录音频内容的通道数。指定默认值1意味着使用单声道录制,设置2表示使用立体声录制。除非使用外部硬件进行录制,否则通常应该创建单声道录音。

4.1.4 指定格式的键

处理Linear PCM或压缩音频格式时,可以定义一些其他指定格式的键。可在AVFoundation->AVFAudio->AVAudioSettings中找到完整的列表。

4.2 控制录音过程

AVAudioRecorder包含一些方法可以支持无限时长的录制,比如在未来某一时间点开始录制或录制指定时长的内容等。开发者可以暂停录音并在停止的地方继续录制。

5.一个简单的录音控制器

5.1 配置音频会话

音频会话默认是AVAudioSession.Category.soloAmbient,这个会话只支持播放,并且在锁屏等时候会静音,单独需要录音的话,我们可以使用.record分类,不过我们既想播放音频也想录音的话,使用.playAndRecord是个很好的选择

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let session = AVAudioSession.sharedInstance()
    do {
       try session.setCategory(.playAndRecord)
    } catch let error {
       print(error)
    }
    do {
       try session.setActive(true)
    } catch let error {
       print(error)
    }
        
    return true
}

5.2 音频录制器代码

import AVFoundation

struct LWMemo {
    var name: String
    var url: URL
    init(_ name: String,_ url: URL) {
        self.name = name
        self.url = url
    }
}

class LWAudioRecorderController: NSObject {
    
    //MARK: 音频录制的属性与方法
    /*
     格式化时间
     */
    var formattedCurrentTime: String {
        ///currentTime是音频文件从开始的时间
        let time = UInt(recorder?.currentTime ?? 0)
        let hours = time/3600
        let minutes = (time/60)%60
        let seconds = time%60
        var formatString = ""
        if hours > 0 {
            formatString += String(format: "%02i:", hours)
        }
        if minutes > 0 {
            formatString += String(format: "%02i", minutes)
        }
        if seconds > 0 {
            formatString += String(format: "%02i", seconds)
        }
        if formatString.count == 0 {
            formatString = "00:00"
        }
        return formatString
    }
    
    func record () -> Bool {
        recorder?.record() ?? false
    }
    func pause(){
        recorder?.pause()
    }
    
    func stop(_ completion: @escaping (Bool) -> Void){
        completionHanlder = completion
        /*
        调用stop之后会触发协议的audioRecorderDidFinishRecording
        方法
        */
        recorder?.stop()
    }
    func saveRecording(_ name: String,_ completion: ((Bool,Any?) -> Void)?){
        /*
         该方法中完成录音保存的功能
         */
        let timeStamp = Date.timeIntervalSinceReferenceDate
        let fileName = name+"-"+"\(timeStamp)"+".caf"
        let destinationPath = kPathDoucument + "/\(fileName)"
        guard let sourceUrl = recorder?.url else {
            return
        }
        let destionationUrl = URL(fileURLWithPath: destinationPath)
        
        do {
            try FileManager.default.copyItem(at: sourceUrl, to: destionationUrl)
            completion?(true,LWMemo(name, destionationUrl))
        } catch let error {
            completion?(false,error)
        }
        
    }
    
    private var recorder: AVAudioRecorder?
    private var completionHanlder: ((Bool)->Void)?
    
    override init() {
        super.init()
        self.configInit()
    }
    private func configInit() {
        /*
         1.将录音存放到tmp目录中名为memo.caf的文件
         2.使用Core Audio Format(CAF)作为容器格式
           因为它和内容无关并可以保存Core Audo支持的
           任何音频格式
         3.使用AppleIMA4作为音频格式
         4.采样率设置为44.1kHz
         5.位深设置为16位
         6.单声道录制
        */
        let filePath = kPathTemp + "memo.caf"
        let url = URL(fileURLWithPath: filePath)
        
        let settings = [AVFormatIDKey: kAudioFormatAppleIMA4,AVSampleRateKey: 44100.0,AVNumberOfChannelsKey:1,AVEncoderBitDepthHintKey: 16,AVEncoderAudioQualityKey: AVAudioQuality.medium] as [String: Any]
        do {
            try recorder = AVAudioRecorder(url: url, settings: settings)
        } catch let error {
            print(error)
        }
        recorder?.delegate = self
        recorder?.prepareToRecord()
    }
    
    //MARK: 音频播放的属性与方法
    var player: AVAudioPlayer?
    
    func playback(_ memo: LWMemo) -> Bool{
        player?.stop()
        player = try? AVAudioPlayer(contentsOf: memo.url)
        if player?.prepareToPlay() ?? false {
            player?.play()
        }
        
        return player != nil
    }
}
extension LWAudioRecorderController: AVAudioRecorderDelegate{
    //录制结束
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool){
        completionHanlder?(flag)
    }
    //发生编码错误的回调
    func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?){

    }
    //录音被打断
    func audioRecorderBeginInterruption(_ recorder: AVAudioRecorder){

    }
    //录音结束被打断
    func audioRecorderEndInterruption(_ recorder: AVAudioRecorder, withOptions flags: Int){

    }
}

以上播放器代码注意点:
我们使用formattedCurrentTime的只读计算属性返回格式化的录音时间,如果需要实时更新展示,需要我们自定义一个每秒定时器,实时的展示时间

6. 音频测量

AVAudioRecorderAVAudioPlayer最强大和最实用的功能就是对音频进行测量。Audio Metering可让开发者读取音频的平均分贝和峰值分贝数据。

两个类都是使用如下两个方法来返回分贝(dB)等级的浮点值,这个值的范围从表示最大分贝的0Db(full scale)到表示最小分贝或静音的-160dB

//返回峰值分贝
open func peakPower(forChannel channelNumber: Int) -> Float
//返回平均分贝
open func averagePower(forChannel channelNumber: Int) -> Float

在读取这些值之前,我们需要首先将isMeteringEnabled属性设置为true才能支持对音频进行测量,然后我们调用updateMeters()方法来获取最新的值。

//先设置允许测量
recorder?.isMeteringEnabled = true
//更新测量数据
recorder?.updateMeters()
//得到测量数据平均值
recorder?.averagePower(forChannel: 0)
//得到测量数据峰值
recorder?.peakPower(forChannel: 0)

声道索引都是以0开始的,由于我们单声道录制,只需要询问第一个声道即可

不断的读取音频强度值和5.2中一样,需要设置一个定时器,不断的获取音频强度数据,不过由于我们希望频繁更新用于展示计量值以保持动画效果比较平滑,所以可以改用CADisplayLink作为解决方案。

关于声音计量展示需要注意的一点是,这么做会增加开销。启用计量功能会导致一些额外计算,会影响设备的耗电量。所以如果录制长时间的音频内容,可能需要考虑禁用音频计量功能。默认isMeteringEnabled就是false,也就是默认就是禁用的。

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