AudioFileStream

AudioFileStream介绍

AudioFileStreamer时提到它的作用是用来读取采样率、码率、时长等基本信息以及分离音频帧。那么在官方文档中Apple是这样描述的:

To play streamed audio content, such as from a network connection, use Audio File Stream Services in concert with Audio Queue Services. Audio File Stream Services parses audio packets and metadata from common audio file container formats in a network bitstream. You can also use it to parse packets and metadata from on-disk files

根据Apple的描述AudioFileStreamer用在流播放中,当然不仅限于网络流,本地文件同样可以用它来读取信息和分离音频帧。AudioFileStreamer的主要数据是文件数据而不是文件路径,所以数据的读取需要使用者自行实现,

支持的文件格式有:

MPEG-1 Audio Layer 3, used for .mp3 files

MPEG-2 ADTS, used for the .aac audio data format

AIFC

AIFF

CAF

MPEG-4, used for .m4a, .mp4, and .3gp files

NeXT

WAVE

上述格式是iOS、MacOSX所支持的音频格式,这类格式可以被系统提供的API解码,如果想要解码其他的音频格式(如OGG、APE、FLAC)就需要自己实现解码器了。

初始化AudioFileStream

第一步,自然是要生成一个AudioFileStream的实例:

12345externOSStatusAudioFileStreamOpen(void*inClientData,AudioFileStream_PropertyListenerProcinPropertyListenerProc,AudioFileStream_PacketsProcinPacketsProc,AudioFileTypeIDinFileTypeHint,AudioFileStreamID*outAudioFileStream);

第一个参数和之前的AudioSession的初始化方法一样是一个上下文对象;

第二个参数AudioFileStream_PropertyListenerProc是歌曲信息解析的回调,每解析出一个歌曲信息都会进行一次回调;

第三个参数AudioFileStream_PacketsProc是分离帧的回调,每解析出一部分帧就会进行一次回调;

第四个参数AudioFileTypeID是文件类型的提示,这个参数来帮助AudioFileStream对文件格式进行解析。这个参数在文件信息不完整(例如信息有缺陷)时尤其有用,它可以给与AudioFileStream一定的提示,帮助其绕过文件中的错误或者缺失从而成功解析文件。所以在确定文件类型的情况下建议各位还是填上这个参数,如果无法确定可以传入0(原理上应该和这篇博文近似);

1234567891011121314151617181920//AudioFileTypeID枚举enum{kAudioFileAIFFType='AIFF',kAudioFileAIFCType='AIFC',kAudioFileWAVEType='WAVE',kAudioFileSoundDesigner2Type='Sd2f',kAudioFileNextType='NeXT',kAudioFileMP3Type='MPG3',// mpeg layer 3kAudioFileMP2Type='MPG2',// mpeg layer 2kAudioFileMP1Type='MPG1',// mpeg layer 1kAudioFileAC3Type='ac-3',kAudioFileAAC_ADTSType='adts',kAudioFileMPEG4Type='mp4f',kAudioFileM4AType='m4af',kAudioFileM4BType='m4bf',kAudioFileCAFType='caff',kAudioFile3GPType='3gpp',kAudioFile3GP2Type='3gp2',kAudioFileAMRType='amrf'};

第五个参数是返回的AudioFileStream实例对应的AudioFileStreamID,这个ID需要保存起来作为后续一些方法的参数使用;

返回值用来判断是否成功初始化(OSStatus == noErr)。

解析数据

在初始化完成之后,只要拿到文件数据就可以进行解析了。解析时调用方法:

1234externOSStatusAudioFileStreamParseBytes(AudioFileStreamIDinAudioFileStream,UInt32inDataByteSize,constvoid*inData,UInt32inFlags);

第一个参数AudioFileStreamID,即初始化时返回的ID;

第二个参数inDataByteSize,本次解析的数据长度;

第三个参数inData,本次解析的数据;

第四个参数是说本次的解析和上一次解析是否是连续的关系,如果是连续的传入0,否则传入kAudioFileStreamParseFlag_Discontinuity。

这里需要插入解释一下何谓“连续”。在第一篇中我们提到过形如MP3的数据都以帧的形式存在的,解析时也需要以帧为单位解析。但在解码之前我们不可能知道每个帧的边界在第几个字节,所以就会出现这样的情况:我们传给AudioFileStreamParseBytes的数据在解析完成之后会有一部分数据余下来,这部分数据是接下去那一帧的前半部分,如果再次有数据输入需要继续解析时就必须要用到前一次解析余下来的数据才能保证帧数据完整,所以在正常播放的情况下传入0即可。目前知道的需要传入kAudioFileStreamParseFlag_Discontinuity的情况有两个,一个是在seek完毕之后显然seek后的数据和之前的数据完全无关;另一个是开源播放器AudioStreamer的作者@Matt Gallagher曾在自己的blog中提到过的:

the Audio File Stream Services hit me with a nasty bug: AudioFileStreamParseBytes will crash when trying to parse a streaming MP3.

In this case, if we pass the kAudioFileStreamParseFlag_Discontinuity flag to AudioFileStreamParseBytes on every invocation between receiving kAudioFileStreamProperty_ReadyToProducePackets and the first successful call to MyPacketsProc, then AudioFileStreamParseBytes will be extra cautious in its approach and won't crash.

Matt发布这篇blog是在2008年,这个Bug年代相当久远了,而且原因未知,究竟是否修复也不得而知,而且由于环境不同(比如测试用的mp3文件和所处的iOS系统)无法重现这个问题,所以我个人觉得还是按照Matt的work around在回调得到kAudioFileStreamProperty_ReadyToProducePackets之后,在正常解析第一帧之前都传入kAudioFileStreamParseFlag_Discontinuity比较好。

回到之前的内容,AudioFileStreamParseBytes方法的返回值表示当前的数据是否被正常解析,如果OSStatus的值不是noErr则表示解析不成功,其中错误码包括:

123456789101112131415enum{kAudioFileStreamError_UnsupportedFileType='typ?',kAudioFileStreamError_UnsupportedDataFormat='fmt?',kAudioFileStreamError_UnsupportedProperty='pty?',kAudioFileStreamError_BadPropertySize='!siz',kAudioFileStreamError_NotOptimized='optm',kAudioFileStreamError_InvalidPacketOffset='pck?',kAudioFileStreamError_InvalidFile='dta?',kAudioFileStreamError_ValueUnknown='unk?',kAudioFileStreamError_DataUnavailable='more',kAudioFileStreamError_IllegalOperation='nope',kAudioFileStreamError_UnspecifiedError='wht?',kAudioFileStreamError_DiscontinuityCantRecover='dsc!'};

大多数都可以从字面上理解,需要提一下的是kAudioFileStreamError_NotOptimized,文档上是这么说的:

It is not possible to produce output packets because the file's packet table or other defining info is either not present or is after the audio data.

它的含义是说这个音频文件的文件头不存在或者说文件头可能在文件的末尾,当前无法正常Parse,换句话说就是这个文件需要全部下载完才能播放,无法流播。

注意AudioFileStreamParseBytes方法每一次调用都应该注意返回值,一旦出现错误就可以不必继续Parse了。

解析文件格式信息

在调用AudioFileStreamParseBytes方法进行解析时会首先读取格式信息,并同步的进入AudioFileStream_PropertyListenerProc回调方法

来看一下这个回调方法的定义

1234typedefvoid(*AudioFileStream_PropertyListenerProc)(void*inClientData,AudioFileStreamIDinAudioFileStream,AudioFileStreamPropertyIDinPropertyID,UInt32*ioFlags);

回调的第一个参数是Open方法中的上下文对象;

第二个参数inAudioFileStream是和Open方法中第四个返回参数AudioFileStreamID一样,表示当前FileStream的ID;

第三个参数是此次回调解析的信息ID。表示当前PropertyID对应的信息已经解析完成信息(例如数据格式、音频数据的偏移量等等),使用者可以通过AudioFileStreamGetProperty接口获取PropertyID对应的值或者数据结构;

1234externOSStatusAudioFileStreamGetProperty(AudioFileStreamIDinAudioFileStream,AudioFileStreamPropertyIDinPropertyID,UInt32*ioPropertyDataSize,void*outPropertyData);

第四个参数ioFlags是一个返回参数,表示这个property是否需要被缓存,如果需要赋值kAudioFileStreamPropertyFlag_PropertyIsCached否则不赋值(这个参数我也不知道应该在啥场景下使用。。一直都没去理他);

这个回调会进来多次,但并不是每一次都需要进行处理,可以根据需求处理需要的PropertyID进行处理(PropertyID列表如下)。

1234567891011121314151617181920212223//AudioFileStreamProperty枚举enum{kAudioFileStreamProperty_ReadyToProducePackets='redy',kAudioFileStreamProperty_FileFormat='ffmt',kAudioFileStreamProperty_DataFormat='dfmt',kAudioFileStreamProperty_FormatList='flst',kAudioFileStreamProperty_MagicCookieData='mgic',kAudioFileStreamProperty_AudioDataByteCount='bcnt',kAudioFileStreamProperty_AudioDataPacketCount='pcnt',kAudioFileStreamProperty_MaximumPacketSize='psze',kAudioFileStreamProperty_DataOffset='doff',kAudioFileStreamProperty_ChannelLayout='cmap',kAudioFileStreamProperty_PacketToFrame='pkfr',kAudioFileStreamProperty_FrameToPacket='frpk',kAudioFileStreamProperty_PacketToByte='pkby',kAudioFileStreamProperty_ByteToPacket='bypk',kAudioFileStreamProperty_PacketTableInfo='pnfo',kAudioFileStreamProperty_PacketSizeUpperBound='pkub',kAudioFileStreamProperty_AverageBytesPerPacket='abpp',kAudioFileStreamProperty_BitRate='brat',kAudioFileStreamProperty_InfoDictionary='info'};

这里列几个我认为比较重要的PropertyID:

1、kAudioFileStreamProperty_BitRate:

表示音频数据的码率,获取这个Property是为了计算音频的总时长Duration(因为AudioFileStream没有这样的接口。。)。

1234567UInt32bitRate;UInt32bitRateSize=sizeof(bitRate);OSStatusstatus=AudioFileStreamGetProperty(inAudioFileStream,kAudioFileStreamProperty_BitRate,&bitRateSize,&bitRate);if(status!=noErr){//错误处理}

2014.8.2 补充:发现在流播放的情况下,有时数据流量比较小时会出现ReadyToProducePackets还是没有获取到bitRate的情况,这时就需要分离一些拼音帧然后计算平均bitRate,计算公式如下:

1UInt32averageBitRate=totalPackectByteCount/totalPacketCout;

2、kAudioFileStreamProperty_DataOffset:

表示音频数据在整个音频文件中的offset(因为大多数音频文件都会有一个文件头之后才使真正的音频数据),这个值在seek时会发挥比较大的作用,音频的seek并不是直接seek文件位置而seek时间(比如seek到2分10秒的位置),seek时会根据时间计算出音频数据的字节offset然后需要再加上音频数据的offset才能得到在文件中的真正offset。

1234567SInt64dataOffset;UInt32offsetSize=sizeof(dataOffset);OSStatusstatus=AudioFileStreamGetProperty(inAudioFileStream,kAudioFileStreamProperty_DataOffset,&offsetSize,&dataOffset);if(status!=noErr){//错误处理}

3、kAudioFileStreamProperty_DataFormat

表示音频文件结构信息,是一个AudioStreamBasicDescription的结构

1234567891011121314151617181920structAudioStreamBasicDescription{Float64mSampleRate;UInt32mFormatID;UInt32mFormatFlags;UInt32mBytesPerPacket;UInt32mFramesPerPacket;UInt32mBytesPerFrame;UInt32mChannelsPerFrame;UInt32mBitsPerChannel;UInt32mReserved;};AudioStreamBasicDescriptionasbd;UInt32asbdSize=sizeof(asbd);OSStatusstatus=AudioFileStreamGetProperty(inAudioFileStream,kAudioFileStreamProperty_DataFormat,&asbdSize,&asbd);if(status!=noErr){//错误处理}

4、kAudioFileStreamProperty_FormatList

作用和kAudioFileStreamProperty_DataFormat是一样的,区别在于用这个PropertyID获取到是一个AudioStreamBasicDescription的数组,这个参数是用来支持AAC SBR这样的包含多个文件类型的音频格式。由于到底有多少个format我们并不知晓,所以需要先获取一下总数据大小:

123456789101112131415161718192021222324//获取数据大小BooleanoutWriteable;UInt32formatListSize;OSStatusstatus=AudioFileStreamGetPropertyInfo(inAudioFileStream,kAudioFileStreamProperty_FormatList,&formatListSize,&outWriteable);if(status!=noErr){//错误处理}//获取formatlistAudioFormatListItem*formatList=malloc(formatListSize);OSStatusstatus=AudioFileStreamGetProperty(inAudioFileStream,kAudioFileStreamProperty_FormatList,&formatListSize,formatList);if(status!=noErr){//错误处理}//选择需要的格式for(inti=0;i*sizeof(AudioFormatListItem)

5、kAudioFileStreamProperty_AudioDataByteCount

顾名思义,音频文件中音频数据的总量。这个Property的作用一是用来计算音频的总时长,二是可以在seek时用来计算时间对应的字节offset。

1234567UInt64audioDataByteCount;UInt32byteCountSize=sizeof(audioDataByteCount);OSStatusstatus=AudioFileStreamGetProperty(inAudioFileStream,kAudioFileStreamProperty_AudioDataByteCount,&byteCountSize,&audioDataByteCount);if(status!=noErr){//错误处理}

2014.8.2 补充:发现在流播放的情况下,有时数据流量比较小时会出现ReadyToProducePackets还是没有获取到audioDataByteCount的情况,这时就需要近似计算audioDataByteCount。一般来说音频文件的总大小一定是可以得到的(利用文件系统或者Http请求中的contentLength),那么计算方法如下:

123UInt32dataOffset=...;//kAudioFileStreamProperty_DataOffsetUInt32fileLength=...;//音频文件大小UInt32audioDataByteCount=fileLength-dataOffset;

5、kAudioFileStreamProperty_ReadyToProducePackets

这个PropertyID可以不必获取对应的值,一旦回调中这个PropertyID出现就代表解析完成,接下来可以对音频数据进行帧分离了。

计算时长Duration

获取时长的最佳方法是从ID3信息中去读取,那样是最准确的。如果ID3信息中没有存,那就依赖于文件头中的信息去计算了。

计算duration的公式如下:

1doubleduration=(audioDataByteCount*8)/bitRate

音频数据的字节总量audioDataByteCount可以通过kAudioFileStreamProperty_AudioDataByteCount获取,码率bitRate可以通过kAudioFileStreamProperty_BitRate获取也可以通过Parse一部分数据后计算平均码率来得到。

对于CBR数据来说用这样的计算方法的duration会比较准确,对于VBR数据就不好说了。所以对于VBR数据来说,最好是能够从ID3信息中获取到duration,获取不到再想办法通过计算平均码率的途径来计算duration。

分离音频帧

读取格式信息完成之后继续调用AudioFileStreamParseBytes方法可以对帧进行分离,并同步的进入AudioFileStream_PacketsProc回调方法。

回调的定义:

12345typedefvoid(*AudioFileStream_PacketsProc)(void*inClientData,UInt32numberOfBytes,UInt32numberOfPackets,constvoid*inInputData,AudioStreamPacketDescription*inPacketDescriptions);

第一个参数,一如既往的上下文对象;

第二个参数,本次处理的数据大小;

第三个参数,本次总共处理了多少帧(即代码里的Packet);

第四个参数,本次处理的所有数据;

第五个参数,AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节。

12345678//AudioStreamPacketDescription结构//这里的mVariableFramesInPacket是指实际的数据帧只有VBR的数据才能用到(像MP3这样的压缩数据一个帧里会有好几个数据帧)structAudioStreamPacketDescription{SInt64mStartOffset;UInt32mVariableFramesInPacket;UInt32mDataByteSize;};

下面是我按照自己的理解实现的回调方法片段:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051staticvoidMyAudioFileStreamPacketsCallBack(void*inClientData,UInt32numberOfBytes,UInt32numberOfPackets,constvoid*inInputData,AudioStreamPacketDescription*inPacketDescriptions){//处理discontinuous..if(numberOfBytes==0||numberOfPackets==0){return;}BOOLdeletePackDesc=NO;if(packetDescriptioins==NULL){//如果packetDescriptioins不存在,就按照CBR处理,平均每一帧的数据后生成packetDescriptioinsdeletePackDesc=YES;UInt32packetSize=numberOfBytes/numberOfPackets;packetDescriptioins=(AudioStreamPacketDescription*)malloc(sizeof(AudioStreamPacketDescription)*numberOfPackets);for(inti=0;i

inPacketDescriptions这个字段为空时需要按CBR的数据处理。但其实在解析CBR数据时inPacketDescriptions一般也会有返回,因为即使是CBR数据帧的大小也不是恒定不变的,例如CBR的MP3会在每一帧的数据后放1 byte的填充位,这个填充位也并非时时刻刻存在,所以帧的大小会有1 byte的浮动。(比如采样率44.1KHZ,码率160kbps的CBR MP3文件每一帧的大小在522字节和523字节浮动。所以不能因为有inPacketDescriptions没有返回NULL而判定音频数据就是VBR编码的)。

Seek

就音频的角度来seek功能描述为“我要拖到xx分xx秒”,而实际操作时我们需要操作的是文件,所以我们需要知道的是“我要拖到xx分xx秒”这个操作对应到文件上是要从第几个字节开始读取音频数据。

对于原始的PCM数据来说每一个PCM帧都是固定长度的,对应的播放时长也是固定的,但一旦转换成压缩后的音频数据就会因为编码形式的不同而不同了。对于CBR而言每个帧中所包含的PCM数据帧是恒定的,所以每一帧对应的播放时长也是恒定的;而VBR则不同,为了保证数据最优并且文件大小最小,VBR的每一帧中所包含的PCM数据帧是不固定的,这就导致在流播放的情况下VBR的数据想要做seek并不容易。这里我们也只讨论CBR下的seek。

CBR数据的seek一般是这样实现的(参考并修改自matt的blog):

1、近似地计算应该seek到哪个字节

1234567doubleseekToTime=...;//需要seek到哪个时间,秒为单位UInt64audioDataByteCount=...;//通过kAudioFileStreamProperty_AudioDataByteCount获取的值SInt64dataOffset=...;//通过kAudioFileStreamProperty_DataOffset获取的值doubledurtion=...;//通过公式(AudioDataByteCount * 8) / BitRate计算得到的时长//近似seekOffset = 数据偏移 + seekToTime对应的近似字节数SInt64approximateSeekOffset=dataOffset+(seekToTime/duration)*audioDataByteCount;

2、计算seekToTime对应的是第几个帧(Packet)

我们可以利用之前Parse得到的音频格式信息来计算PacketDuration。audioItem.fileFormat.mFramesPerPacket /audioItem.fileFormat.mSampleRate;

123456//首先需要计算每个packet对应的时长AudioStreamBasicDescriptionasbd=...;////通过kAudioFileStreamProperty_DataFormat或者kAudioFileStreamProperty_FormatList获取的值doublepacketDuration=asbd.mFramesPerPacket/asbd.mSampleRate//然后计算packet位置SInt64seekToPacket=floor(seekToTime/packetDuration);

3、使用AudioFileStreamSeek计算精确的字节偏移和时间

AudioFileStreamSeek可以用来寻找某一个帧(Packet)对应的字节偏移(byte offset):

如果ioFlags里有kAudioFileStreamSeekFlag_OffsetIsEstimated说明给出的outDataByteOffset是估算的,并不准确,那么还是应该用第1步计算出来的approximateSeekOffset来做seek;

如果ioFlags里没有kAudioFileStreamSeekFlag_OffsetIsEstimated说明给出了准确的outDataByteOffset,就是输入的seekToPacket对应的字节偏移量,我们可以根据outDataByteOffset来计算出精确的seekOffset和seekToTime;

1234567891011121314SInt64seekByteOffset;UInt32ioFlags=0;SInt64outDataByteOffset;OSStatusstatus=AudioFileStreamSeek(audioFileStreamID,seekToPacket,&outDataByteOffset,&ioFlags);if(status==noErr&&!(ioFlags&kAudioFileStreamSeekFlag_OffsetIsEstimated)){//如果AudioFileStreamSeek方法找到了准确的帧字节偏移,需要修正一下时间seekToTime-=((approximateSeekOffset-dataOffset)-outDataByteOffset)*8.0/bitRate;seekByteOffset=outDataByteOffset+dataOffset;}else{seekByteOffset=approximateSeekOffset;}

4、按照seekByteOffset读取对应的数据继续使用AudioFileStreamParseByte进行解析

如果是网络流可以通过设置range头来获取字节,本地文件的话直接seek就好了。调用AudioFileStreamParseByte时注意刚seek完第一次Parse数据需要加参数kAudioFileStreamParseFlag_Discontinuity。

关闭AudioFileStream

AudioFileStream使用完毕后需要调用AudioFileStreamClose进行关闭,没啥特别需要注意的。

1externOSStatusAudioFileStreamClose(AudioFileStreamIDinAudioFileStream);

小结

本篇关于AudioFileStream做了详细介绍,小结一下:

使用AudioFileStream首先需要调用AudioFileStreamOpen,需要注意的是尽量提供inFileTypeHint参数帮助AudioFileStream解析数据,调用完成后记录AudioFileStreamID;

当有数据时调用AudioFileStreamParseBytes进行解析,每一次解析都需要注意返回值,返回值一旦出现noErr以外的值就代表Parse出错,其中kAudioFileStreamError_NotOptimized代表该文件缺少头信息或者其头信息在文件尾部不适合流播放;

使用AudioFileStreamParseBytes需要注意第四个参数在需要合适的时候传入kAudioFileStreamParseFlag_Discontinuity;

调用AudioFileStreamParseBytes后会首先同步进入AudioFileStream_PropertyListenerProc回调来解析文件格式信息,如果回调得到kAudioFileStreamProperty_ReadyToProducePackets表示解析格式信息完成;

解析格式信息完成后继续调用AudioFileStreamParseBytes会进入MyAudioFileStreamPacketsCallBack回调来分离音频帧,在回调中应该将分离出来的帧信息保存到自己的buffer中

seek时需要先近似的计算seekTime对应的seekByteOffset,然后利用AudioFileStreamSeek计算精确的offset,如果能得到精确的offset就修正一下seektime,如果无法得到精确的offset就用之前的近似结果

AudioFileStream使用完毕后需要调用AudioFileStreamClose进行关闭;

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

推荐阅读更多精彩内容