算法101, swift , https://juejin.im/user/599fe9216fb9a0249d616ba8

Swift 音频 DIY ,Audio Queue Services 搞缓冲,AVAudioEngine 加声效


播放网络音频,可以先下载好,得到音频文件,简单了
使用 AVAudioPlayer 播放,就完
苹果封装下,AVAudioPlayer 处理本地文件,很方便

直接拿到一个文件地址 url,播放

简单机械的理解:
便于音频的传输,一般使用音频压缩文件,mp3 等。文件压的体积小,好传输
声卡是播放 PCM 缓冲的
苹果帮开发把压缩格式,转换为未压缩的原始文件 PCM, 还帮开发做播放音频的资源调度,从 PCM 文件中拿出一段段的缓冲 buffer,交给声卡消费掉

( 实际不会分两步,过程当然是并行的 )

现在手动

本文介绍,直接搞音频流媒体

接收到网络上的音频数据包,就去播放。


68747470733a2f2f63646e2e666173746c6561726e65722e6d656469612f73747265616d65722d6f766572766965772d6469616772616d2e737667.png

分四步:

1,网络的音频文件 >> 下载到本地的音频 data

下载音频文件的二进制数据
URLSession 建立 task, 去获取网络文件
拿到一个数据包 Data,就处理一个
本例子中,一个数据包 Data,对应一个音频包 packet, 对应一个音频缓冲 buffer

这一步,比较容易,
建个 URLSessionDataTask ,去下载

要做的,都在网络代理方法里


extension Downloader: URLSessionDataDelegate {
// 开始下载,拿到文件的总体积
   public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
       totalBytesCount = response.expectedContentLength
       completionHandler(.allow)
   }

// 接收数据
   public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
       // 更新,下载到本地的数据总量
       totalBytesReceived += Int64(data.count)
       // 算进度
       progress = Float(totalBytesReceived) / Float(totalBytesCount)
       // data 教给代理,去解析为音频数据包
       delegate?.download(self, didReceiveData: data, progress: progress)
   }
   
   // 下载完成了
   public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
       state = .completed
       delegate?.download(self, completedWithError: error)
   }
}


音频基础了解先:

音频文件,分为封装格式(文件格式),和编码格式

音频数据的三个层级,buffer, packet, frame

数据缓冲 buffer , 装音频包 packet,
音频包 packet,装音频帧 frame

音频按编码格式,一般分为可变码率 ,和固定码率

固定码率 CBR, 平均采样,对应原始文件,pcm ( 未压缩文件 )

可变码率 VBR,对应压缩文件,例如: mp3
Core Audio 支持 VBR,一般通过可变帧率格式 VFR

VFR 是指:每个包 packet 的体积相等,
包 packet 里面的帧 frame 的数量不一, 帧 frame 含有的音频数据有大有小

Core Audio 中数据描述

固定码率用 ASBD 描述,AudioStreamBasicDescription
ASBD 的描述, 就是指一些配置信息,包含通道数、采样率、位深...

可变码率中 VFR,用 ASPD 描述,AudioStreamPacketDescription
压缩音频数据中 VFR,对应 ASPD
每一个包 Packet,都有其 ASPD

ASPD 里面有,包 packet 的位置信息 mStartOffset
包 packet 的帧 frame 的个数,mVariableFramesInPacket


68747470733a2f2f63646e2e666173746c6561726e65722e6d656469612f71756575652d73657276696365732d6469616772616d2e737667.png

2,音频 data >> 音频包 Packet

拿 Audio Queue Services ,处理上一步获取的音频二进制数据 data,解析为音频数据包 packet

2.1 建立音频的处理通道, 注册解析回调方法

public init() throws {
        let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        // 创建一个活跃的音频文件流解析器,创建解析器 ID
        guard AudioFileStreamOpen(context, ParserPropertyChangeCallback, ParserPacketCallback, kAudioFileMP3Type, &streamID) == noErr else {
            throw ParserError.streamCouldNotOpen
        }
    }

2.2 传递数据进来,开始解析

    public func parse(data: Data) throws {
        let streamID = self.streamID!
        let count = data.count
        _ = try data.withUnsafeBytes({ (rawBufferPointer) in
            let bufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
            if let address = bufferPointer.baseAddress{
                // 把音频数据,传给解析器
                //  streamID,  指定解析器
                let result = AudioFileStreamParseBytes(streamID, UInt32(count), address, [])
                guard result == noErr else {
                    throw ParserError.failedToParseBytes(result)
                }
            }
        })
    }

2.3 音频信息解析先

func ParserPropertyChangeCallback(_ context: UnsafeMutableRawPointer, _ streamID: AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID, _ flags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
    let parser = Unmanaged<Parser>.fromOpaque(context).takeUnretainedValue()
    // 关心什么信息,取什么
    switch propertyID {
    case kAudioFileStreamProperty_DataFormat:
        // 拿数据格式
        var format = AudioStreamBasicDescription()
        GetPropertyValue(&format, streamID, propertyID)
        parser.dataFormat = AVAudioFormat(streamDescription: &format)

    case kAudioFileStreamProperty_AudioDataPacketCount:
         // 音频流文件,分离出来的音频数据中,的包 packet 个数
        GetPropertyValue(&parser.packetCount, streamID, propertyID)

    default:
         () 
    }
}

// 套路就是,先拿内存大小 propSize, 再拿关心的属性的值 value
func GetPropertyValue<T>(_ value: inout T, _ streamID: AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID) {
    var propSize: UInt32 = 0
    guard AudioFileStreamGetPropertyInfo(streamID, propertyID, &propSize, nil) == noErr else {
        return
    }
    guard AudioFileStreamGetProperty(streamID, propertyID, &propSize, &value) == noErr else {
        return
    }
}

2.4 解析回调,处理数据

func ParserPacketCallback(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ data: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {

    // 拿回了 self ( parser )
    let parser = Unmanaged<Parser>.fromOpaque(context).takeUnretainedValue()
    let packetDescriptionsOrNil: UnsafeMutablePointer<AudioStreamPacketDescription>? = packetDescriptions
    // ASPD 存在,就是压缩的音频包
    // 未压缩的 pcm, 使用 ASBD
    let isCompressed = packetDescriptionsOrNil != nil
    guard let dataFormat = parser.dataFormat else {
        return
    }
    
    // 拿到了数据,遍历,
    // 存储进去 parser.packets, 也就是 self.packets
    if isCompressed {
        for i in 0 ..< Int(packetCount) {
            // 压缩音频数据,每一个包对应 ASPD, 逐个计算
            let packetDescription = packetDescriptions[i]
            let packetStart = Int(packetDescription.mStartOffset)
            let packetSize = Int(packetDescription.mDataByteSize)
            let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)
            parser.packets.append((packetData, packetDescription))
        }
    } else {
         // 原始音频数据 pcm,文件统一配置,计算比较简单
        let format = dataFormat.streamDescription.pointee
        let bytesPerPacket = Int(format.mBytesPerPacket)
        for i in 0 ..< Int(packetCount) {
            let packetStart = i * bytesPerPacket
            let packetSize = bytesPerPacket
            let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)
            parser.packets.append((packetData, nil))
        }
    }
}

3,音频包 packet >> 音频缓冲 buffer

public required init(parser: Parsing, readFormat: AVAudioFormat) throws {
        // 从之前负责解析的,拿音频数据
        self.parser = parser
        
        guard let dataFormat = parser.dataFormat else {
            throw ReaderError.parserMissingDataFormat
        }

        let sourceFormat = dataFormat.streamDescription
        let commonFormat = readFormat.streamDescription
        // 创建音频格式转换器 converter
        // 通过指定输入格式,和输出格式
        // 输入格式是上一步解析出来的,从 paser 里面拿
        // 输出格式,开发指定的
        let result = AudioConverterNew(sourceFormat, commonFormat, &converter)
        guard result == noErr else {
            throw ReaderError.unableToCreateConverter(result)
        }
        self.readFormat = readFormat
    }
    

开发指定的输出格式

public var readFormat: AVAudioFormat {
        return AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
    }

// 位深,采用 Float32
// 采样率 44100 Hz, 标准 CD 音质
// 分左右声道

上一步解析出音频包 packet 后,进入读取音频缓冲 buffer 的阶段

    
    public func read(_ frames: AVAudioFrameCount) throws -> AVAudioPCMBuffer {
        let framesPerPacket = readFormat.streamDescription.pointee.mFramesPerPacket
        var packets = frames / framesPerPacket
        
       // 创建空白的、指定格式和容量的,音频缓冲 AVAudioPCMBuffer
        guard let buffer = AVAudioPCMBuffer(pcmFormat: readFormat, frameCapacity: frames) else {
            throw ReaderError.failedToCreatePCMBuffer
        }
        buffer.frameLength = frames
        
        // 把解析出的音频包 packet, 转换成 AVAudioPCMBuffer,这样 AVAudioEngine 可以拿来播放
        try queue.sync {
            let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
            // 通过设置好的转换器 converter,使用回调方法 ReaderConverterCallback,填充创建的 buffer 的数据 buffer.mutableAudioBufferList 
            let status = AudioConverterFillComplexBuffer(converter!, ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil)
            guard status == noErr else {
                switch status {
                case ReaderMissingSourceFormatError:
                    throw ReaderError.parserMissingDataFormat
                case ReaderReachedEndOfDataError:
                    throw ReaderError.reachedEndOfFile
                case ReaderNotEnoughDataError:
                    throw ReaderError.notEnoughData
                default:
                    throw ReaderError.converterFailed(status)
                }
            }
        }
        return buffer
    }


  • AudioConverterFillComplexBuffer 的使用姿势:

AudioConverterFillComplexBuffer(格式转换器,回调函数,自定义参数指针,包的个数指针,接收转换后数据的指针,接收 ASPD 的指针)

AudioConverterFillComplexBuffer(converter!, ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil)
  • AudioConverterFillComplexBuffer 的回调函数 ReaderConverterCallback, 的使用姿势:
    回调函数(格式转换器, 包的个数指针,接收转换后数据的指针, 接收 ASPD 的指针, 自定义参数指针 )

可看出,传递给 AudioConverterFillComplexBuffer 的 6 个参数,
除了其回调参数本身,其他 5 个参数,其回调参数都有用到


转换 buffer 的回调函数,之前创建了空白的音频缓冲 buffer,现在往 buffer 里面,填入数据

func ReaderConverterCallback(_ converter: AudioConverterRef,
                             _ packetCount: UnsafeMutablePointer<UInt32>,
                             _ ioData: UnsafeMutablePointer<AudioBufferList>,
                             _ outPacketDescriptions: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?,
                             _ context: UnsafeMutableRawPointer?) -> OSStatus {

    // 还原出 self ( reader )
    let reader = Unmanaged<Reader>.fromOpaque(context!).takeUnretainedValue()
    
    // 确保输入格式可用
    guard let sourceFormat = reader.parser.dataFormat else {
        return ReaderMissingSourceFormatError
    }
    
    //  这个类 Reader, 里面记录了一个播放到的位置 currentPacket, 
    //  播放相对位置,就是一个 offset
    //   判断播放到包尾的情况
     
    //     播放到包尾,根据下载解析情况,分两种情况
    //     1, 下载解析完成,播放到了结尾
    //     2, 下载没完成,解析好了的,都播放完了
    //     (仅此两种状况,因为解析的时间,远比不上下载的时间。下载完成 = 解析完成 )
    let packetIndex = Int(reader.currentPacket)
    let packets = reader.parser.packets
    let isEndOfData = packetIndex >= packets.count - 1
    if isEndOfData {
        if reader.parser.isParsingComplete {
            packetCount.pointee = 0
            return ReaderReachedEndOfDataError
        } else {
            return ReaderNotEnoughDataError
        }
    }
    
    // 之前的设置,一次只处理一个包 packet 的音频数据
    let packet = packets[packetIndex]
    var data = packet.0
    let dataCount = data.count
    ioData.pointee.mNumberBuffers = 1
    // 音频数据拷贝过来:先分配内存,再拷贝地址的数据
    ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: dataCount, alignment: 0)

    _ = data.withUnsafeMutableBytes { (rawMutableBufferPointer) in
        let bufferPointer = rawMutableBufferPointer.bindMemory(to: UInt8.self)
        if let address = bufferPointer.baseAddress{
            memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, address, dataCount)
        }
    }
    
    ioData.pointee.mBuffers.mDataByteSize = UInt32(dataCount)
    
    // 处理压缩文件 MP3, AAC 的 ASPD
    let sourceFormatDescription = sourceFormat.streamDescription.pointee
    if sourceFormatDescription.mFormatID != kAudioFormatLinearPCM {
        if outPacketDescriptions?.pointee == nil {
            outPacketDescriptions?.pointee = UnsafeMutablePointer<AudioStreamPacketDescription>.allocate(capacity: 1)
        }
        outPacketDescriptions?.pointee?.pointee.mDataByteSize = UInt32(dataCount)
        outPacketDescriptions?.pointee?.pointee.mStartOffset = 0
        outPacketDescriptions?.pointee?.pointee.mVariableFramesInPacket = 0
    }
    packetCount.pointee = 1

    // 更新播放到的位置 currentPacket
    reader.currentPacket = reader.currentPacket + 1
    
    return noErr;
}



68747470733a2f2f63646e2e666173746c6561726e65722e6d656469612f6176617564696f656e67696e652d707573682e737667 (1).png

4, 使用 AVAudioEngine, 播放与实时音效处理

AVAudioEngine 可以做实时的音效处理,用 Effect Unit 加效果

4.1 播放先

设置 AudioEngine,添加节点,连接节点

func setupAudioEngine(){
        // 添加节点
        attachNodes()

        // 连接节点
        connectNodes()

        // 准备 AudioEngine
        engine.prepare()
        
        // AVAudioEngine 的数据流,采用推 push 模型
        // 使用计时器,每隔 0.1 秒左右,调度播放资源

        let interval = 1 / (readFormat.sampleRate / Double(readBufferSize))
        let timer = Timer(timeInterval: interval / 2, repeats: true) {
            [weak self] _ in
            guard self?.state != .stopped else {
                return
            }
            // 分配缓冲 buffer, 调度播放资源
            self?.scheduleNextBuffer()
            self?.handleTimeUpdate()
            self?.notifyTimeUpdated()
        }
        RunLoop.current.add(timer, forMode: .common)
    }

    // 添加播放节点
    open func attachNodes() {
        engine.attach(playerNode)
    }

    // 播放节点,连通到输出
    open func connectNodes() {
        engine.connect(playerNode, to: engine.mainMixerNode, format: readFormat)
    }

调度播放资源,将数据 ( 上一步创建的音频缓冲 buffer )交给 AudioEngine 的播放节点 playerNode

func scheduleNextBuffer(){
        guard let reader = reader else {
            return
        }
        //  通过状态记录,管理播放
        // 播放状态,就是一个开关
        guard !isFileSchedulingComplete || repeats else {
            return
        }

        do {
            // 拿到,上一步创建音频缓冲 buffer
            let nextScheduledBuffer = try reader.read(readBufferSize)
            // playerNode 播放消费掉
            playerNode.scheduleBuffer(nextScheduledBuffer)
        } catch ReaderError.reachedEndOfFile {
            isFileSchedulingComplete = true
        } catch {  }
    }

开启播放

public func play() {
        // 没播放,才开启
        guard !playerNode.isPlaying else {
            return
        }
        
        if !engine.isRunning {
            do {
                try engine.start()
            } catch { }
        }
        
        // 提升用户体验,播放前,先静音
        let lastVolume = volumeRampTargetValue ?? volume
        volume = 0
        
        //  播放节点播放
        playerNode.play()
        
        // 250 毫秒后,正常音量播放
        swellVolume(to: lastVolume)
        
        // 更新播放状态
        state = .playing
    }

4.2 音效后

添加实时的音高、播放速度效果

   // 使用 AVAudioUnitTimePitch 单元,调节播放速度和音高效果
    let timePitchNode = AVAudioUnitTimePitch()
    

    override func attachNodes() {
        // 添加播放节点
        super.attachNodes()
        // 添加音效节点
        engine.attach(timePitchNode)
    }
    
// 相当于在播放节点和输出节点中间,插入音效节点
    override func connectNodes() {
        engine.connect(playerNode, to: timePitchNode, format: readFormat)
        engine.connect(timePitchNode, to: engine.mainMixerNode, format: readFormat)
    }


补充细节

5,计算出歌曲的时长, duration

先拿到包的个数,
下载的数据,解析完成后,加出来的

1 首 2:34 秒的 mp3, 可分为 5925 个包

public var totalPacketCount: AVAudioPacketCount? {
        guard let _ = dataFormat else {
            return nil
        }
        // 本例子,走的是 AVAudioPacketCount(packets.count)
        // 2.4 的解析回调 ParserPacketCallback 中,
        // 拿到步骤 1 下载的数据后,就解析,添加数据到 packets
        return max(AVAudioPacketCount(packetCount), AVAudioPacketCount(packets.count))
    }

去拿音频帧 frame 的总数

public var totalFrameCount: AVAudioFrameCount? {
        guard let framesPerPacket = dataFormat?.streamDescription.pointee.mFramesPerPacket else {
            return nil
        }
        
        guard let totalPacketCount = totalPacketCount else {
            return nil
        }
        // 上一步包的总数 X 每个包里有几个帧
        return AVAudioFrameCount(totalPacketCount) * AVAudioFrameCount(framesPerPacket)
    }

计算出音频持续时间

public var duration: TimeInterval? {
        guard let sampleRate = dataFormat?.sampleRate else {
            return nil
        }
        
        guard let totalFrameCount = totalFrameCount else {
            return nil
        }
        // 上一步的音频帧 frame 的总数 / 采样率
        return TimeInterval(totalFrameCount) / TimeInterval(sampleRate)
    }

6,调节播放的当前位置

6.1 音频管理者 streamer 里面
    public func seek(to time: TimeInterval) throws {        
        // 有了 parser 的音频包,和 reader 的音频缓冲,才可播放
        guard let parser = parser, let reader = reader else {
            return
        }
        
        // 拿时间,先算出音频帧的相对位置
        // 拿音频帧的相对位置,算出音频包的相对位置
        guard let frameOffset = parser.frameOffset(forTime: time),
            let packetOffset = parser.packetOffset(forFrame: frameOffset) else {
                return
        }
        // 更新当前状态
        currentTimeOffset = time
        isFileSchedulingComplete = false
        
        // 记录当前状态,一会恢复
        let isPlaying = playerNode.isPlaying
        let lastVolume = volumeRampTargetValue ?? volume
        
        // 优化体验,避免杂声,播放先停下来
        playerNode.stop()
        volume = 0
        
        // 更新 reader 里面的播放资源位置
        do {
            try reader.seek(packetOffset)
        } catch {
            return
        }
        
        // 刚才记录当前状态,恢复
        if isPlaying {
            playerNode.play()
        }
        
        // 更新 UI
        delegate?.streamer(self, updatedCurrentTime: time)
        
        // 恢复原来的音量
        swellVolume(to: lastVolume)
    }

算出当前时间的,帧偏移

   public func frameOffset(forTime time: TimeInterval) -> AVAudioFramePosition? {
        guard let _ = dataFormat?.streamDescription.pointee,
            let frameCount = totalFrameCount,
            let duration = duration else {
                return nil
        }
        //  拿当前时间 / 音频总时长,算出比值
        let ratio = time / duration
        return AVAudioFramePosition(Double(frameCount) * ratio)
    }
算出当前帧,对应的包的位置
    public func packetOffset(forFrame frame: AVAudioFramePosition) -> AVAudioPacketCount? {
        guard let framesPerPacket = dataFormat?.streamDescription.pointee.mFramesPerPacket else {
            return nil
        }
        // 当前是第多少帧 / 一个包里面有几个帧
        return AVAudioPacketCount(frame) / AVAudioPacketCount(framesPerPacket)
    }
6.2 音频资源调度 reader 里面
public func seek(_ packet: AVAudioPacketCount) throws {
        queue.sync {
            // 更改位置偏移
            currentPacket = packet
        }
    }

记录的位置 currentPacket,这样起作用
步骤三的回调 ReaderConverterCallback,

    // ...
    // 本例子中,一个音频包 packet, 对应一个音频缓冲 buffer
    let packet = packets[packetIndex]
    var data = packet.0
    // ...
    _ = data.withUnsafeMutableBytes { (rawMutableBufferPointer) in // ...
   }
   // ...


Screen Shot 2020-02-23 at 11.57.10 PM.png

7 UI 用户体验提升,手动拖拽播放时刻的场景

分三个事件处理:手指按下,手指拖动,手指抬起

//  手指按下, 屏蔽刷新播放进度的代理方法
@IBAction func progressSliderTouchedDown(_ sender: UISlider) {
        isSeeking = true
    }

    //  手指拖动, 屏蔽了刷新播放进度的代理方法,采用手势对应的 UI
    @IBAction func progressSliderValueChanged(_ sender: UISlider) {
        let currentTime = TimeInterval(progressSlider.value)
        currentTimeLabel.text = currentTime.toMMSS()
    }

//  手指抬起, 恢复刷新播放进度的代理方法,这个时候才调度播放的资源
@IBAction func progressSliderTouchedUp(_ sender: UISlider) {
        seek(sender)
        isSeeking = false
    }

相关代理方法,根据播放进度,更新当前事件和进度条的 UI

func streamer(_ streamer: Streaming, updatedCurrentTime currentTime: TimeInterval) {
        if !isSeeking {
            progressSlider.value = Float(currentTime)
            currentTimeLabel.text = currentTime.toMMSS()
        }
    }

8 单曲循环模式

步骤 4 播放中,分发播放资源,是走计时器的

管理下里面的两个方法的逻辑,就好
( 调度音频缓冲,和播放完了改状态 )

let timer = Timer(timeInterval: interval / 2, repeats: true) {
            [weak self] _ in
            // ...
            self?.scheduleNextBuffer()
            self?.handleTimeUpdate()
            // ...
        }

调度音频缓冲 buffer,


func scheduleNextBuffer(){
        guard let reader = reader else {
            return
        }
        // 如果重复 repeats,就继续播放,不用管播放完了一遍没有
        guard !isFileSchedulingComplete || repeats else {
            return
        }
       // ...   下面是,播放节点播放资源
}

根据播放情况,处理相关状态

func handleTimeUpdate(){
        guard let currentTime = currentTime, let duration = duration else {
            return
        }
        // 当前播放的时间,过了音频时长,就认为播放完了一遍,去暂停
        if currentTime >= duration {
            try? seek(to: 0)
            // 如果重复,别暂停
            if !repeats{
                pause()
            }
        }
    }

代码见 github




实例分析:RxSwift 速成的三个原则

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