iOS 音频流式解码器 - AudioFileStream

1 基础知识

AudioFileStream将音频文件流解析为音频数据包的 API。

1.1 文件流错误码类型

音频文件流可能出现的错误类型,部分特殊场景,需要针对特定错误码做处理,完整错误码定义如下:

CF_ENUM(OSStatus)
{
    kAudioFileStreamError_UnsupportedFileType       = 'typ?',    // 不支持指定的文件类型
    kAudioFileStreamError_UnsupportedDataFormat     = 'fmt?',  // 指定的文件类型不支持数据格式
    kAudioFileStreamError_UnsupportedProperty       = 'pty?',    // 不支持该属性 
    kAudioFileStreamError_BadPropertySize           = '!siz',      // 属性数据提供的缓冲区大小不正确
    kAudioFileStreamError_NotOptimized              = 'optm',    // 无法产生输出数据包,因为流式音频文件的数据包表或其他定义信息不存在或出现在音频数据之后
    kAudioFileStreamError_InvalidPacketOffset       = 'pck?',   // 数据包偏移量小于0或超过文件末尾,或者在构建数据包表时读取了损坏的数据包大小
    kAudioFileStreamError_InvalidFile               = 'dta?',       // 文件格式错误,不是其类型的音频文件的有效实例,或未被识别为音频文件
    kAudioFileStreamError_ValueUnknown              = 'unk?',     // 在音频数据之前,此文件中不存在属性值
    kAudioFileStreamError_DataUnavailable           = 'more',     // 提供给解析器的数据量不足以产生任何结果
    kAudioFileStreamError_IllegalOperation          = 'nope',   // 试图进行非法操作
    kAudioFileStreamError_UnspecifiedError          = 'wht?',    // 发生未指明的错误
    kAudioFileStreamError_DiscontinuityCantRecover  = 'dsc!' // 音频数据出现中断,音频文件流服务无法恢复
};

1.2 AudioFileStream Properties

AudioFileStream 中,支持从文件流中获取以下 Property,但不支持给文件设置 Property。完整的 Property定义如下:

CF_ENUM(AudioFileStreamPropertyID)
{
  // UInt32值,在解析器解析到音频数据的开头为止一直为0,当到达音频数据即设置为1,为1时,所有可以知道的音频文件流属性都是已知的。
    kAudioFileStreamProperty_ReadyToProducePackets          =   'redy',
  // 音频文件的格式
    kAudioFileStreamProperty_FileFormat                     =   'ffmt',
  // 音频文件数据格式的结构
    kAudioFileStreamProperty_DataFormat                     =   'dfmt',
  // 为了支持带有SBR的AAC等格式,已编码的数据流可以被解码为多种目标格式,此属性返回一个AudioFormatListItem结构数组,每个目标格式对应一个。
    kAudioFileStreamProperty_FormatList                     =   'flst',
  // 一个指向 magic cookie 的空指针
    kAudioFileStreamProperty_MagicCookieData                =   'mgic',
  // UInt64值,表示流文件中音频数据的字节数。仅当从标头中解析的数据知道整个流的字节数时,此属性才有效。对于某些类型的流,此属性可能没有价值。
    kAudioFileStreamProperty_AudioDataByteCount             =   'bcnt',
  // UInt64值,流文件中的音频数据的数据包的数量的值。
    kAudioFileStreamProperty_AudioDataPacketCount           =   'pcnt',
  // UInt32值,表示所述数据的最大数据包大小值。
    kAudioFileStreamProperty_MaximumPacketSize              =   'psze',
  // SInt64值,表示音频数据开始的流文件中的字节偏移量。
    kAudioFileStreamProperty_DataOffset                     =   'doff',
  // 一个 AudioChannelLayout 数据结构 
    kAudioFileStreamProperty_ChannelLayout                  =   'cmap',
    kAudioFileStreamProperty_PacketToFrame                  =   'pkfr',
    kAudioFileStreamProperty_FrameToPacket                  =   'frpk',
    kAudioFileStreamProperty_RestrictsRandomAccess          =   'rrap',
    kAudioFileStreamProperty_PacketToRollDistance           =   'pkrl',
    kAudioFileStreamProperty_PreviousIndependentPacket      =   'pind',
    kAudioFileStreamProperty_NextIndependentPacket          =   'nind',
    kAudioFileStreamProperty_PacketToDependencyInfo         =   'pkdp',
    kAudioFileStreamProperty_PacketToByte                   =   'pkby',
    kAudioFileStreamProperty_ByteToPacket                   =   'bypk',
    kAudioFileStreamProperty_PacketTableInfo                =   'pnfo',
  // UInt32值,表示指示在流文件中的理论上的最大数据包大小值。例如,此值可用于确定最小缓冲区大小。
    kAudioFileStreamProperty_PacketSizeUpperBound           =   'pkub',
  // Float64值,指示每个数据包的平均字节数。对于 CBR 和带有数据包表的文件,这个数字是准确的。否则,它是解析的数据包的运行平均值。
    kAudioFileStreamProperty_AverageBytesPerPacket          =   'abpp',
  // UInt32值,表示每秒比特数表示流的比特率。
    kAudioFileStreamProperty_BitRate                        =   'brat',
    kAudioFileStreamProperty_InfoDictionary                 =   'info'
};

1.3 AudioFileStream Types

1.3.1 流属性回调类型

解析器在音频文件流中找到属性值时调用。

typedef UInt32 AudioFileStreamPropertyID;
typedef struct OpaqueAudioFileStreamID  *AudioFileStreamID;

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                          inClientData,
                                            AudioFileStreamID               inAudioFileStream,
                                            AudioFileStreamPropertyID       inPropertyID,
                                            AudioFileStreamPropertyFlags *  ioFlags);

inClientData:调用函数时在参数中提供的值;

inAudioFileStream:音频文件流解析器的 ID;

inPropertyID:解析器在音频文件数据流中找到的属性 ID;

ioFlags:在输入时,如果设置了kAudioFileStreamPropertyFlag_PropertyIsCached值,解析器将缓存该属性值。如果不是,可以在输出上设置kAudioFileStreamPropertyFlag_CacheProperty标志,以使解析器缓存该值。参见音频文件流标志。

1.3.2 流数据包回调类型

当音频文件流解析器在音频文件流中找到音频数据时调用。对于恒定比特率 (CBR) 音频数据,通常会使用与传递给函数的数据一样多的数据调用回调。然而,有时由于输入数据的边界,可能只传递一个数据包。对于可变比特率 (VBR) 音频数据,每次调用该函数时可能会多次调用回调。

typedef void (*AudioFileStream_PacketsProc)(
                                            void *                                        inClientData,
                                            UInt32                                        inNumberBytes,
                                            UInt32                                        inNumberPackets,
                                            const void *                                inInputData,
                                            AudioStreamPacketDescription * __nullable   inPacketDescriptions);

inClientData:调用函数时在参数中提供的值;

inNumberBytes:缓冲区中数据的字节数;

inNumberPackets:缓冲区中音频数据的包数;

inInputData:音频数据;

inPacketDescriptions:音频文件流数据包描述结构数组。

1.4 AudioFileStream Flags

音频文件流中标识类型集合:

typedef CF_OPTIONS(UInt32, AudioFileStreamPropertyFlags) {
  // 这个标志是在调用回调AudioFileStream_PropertyListenerProc时设置的,在这种情况下,该属性的值已经被缓存并且可以在以后获得。
    kAudioFileStreamPropertyFlag_PropertyIsCached = 1,
  // 属性侦听器设置此标志以指示解析器缓存属性值,以便在回调返回后它仍然可用。
    kAudioFileStreamPropertyFlag_CacheProperty = 2
};

typedef CF_OPTIONS(UInt32, AudioFileStreamParseFlags) {
  // AudioFileStreamParseBytes方法中,将此标志传递给函数以表示音频数据的不连续性。
    kAudioFileStreamParseFlag_Discontinuity = 1
};

typedef CF_OPTIONS(UInt32, AudioFileStreamSeekFlags) {
  // AudioFileStreamSeek 方法,如果字节偏移量只是一个估计值,则此标志由函数返回。
    kAudioFileStreamSeekFlag_OffsetIsEstimated = 1
};

1.5 AudioFileStream Functions

1.5.1 初始化与释放文件流服务

  1. 创建并打开一个新的音频文件流解析器。
extern OSStatus 
AudioFileStreamOpen (
                            void * __nullable                                    inClientData,
                            AudioFileStream_PropertyListenerProc       inPropertyListenerProc,
                            AudioFileStream_PacketsProc                    inPacketsProc,
              AudioFileTypeID                                        inFileTypeHint,
              AudioFileStreamID __nullable * __nonnull outAudioFileStream);

inClientData:传递给回调函数的值或结构的指针;

inPropertyListenerProc:属性监听器回调,当解析器在数据流中找到Property的值时回调;

inPacketsProc:音频数据回调,当解析器在数据流中找到音频数据包时回调;

inFileTypeHint:音频文件类型,如果不知道音频文件类型,则设置为 0;

outAudioFileStream:音频文件流解析器的 ID,需要将其保存,供其它音频文件流 API 使用。

  1. 关闭并释放指定的音频文件流解析器。
extern OSStatus 
AudioFileStreamClose(AudioFileStreamID inAudioFileStream);

inAudioFileStream:指定的音频文件流解析器的 ID。

1.5.2 解析数据

将音频文件流数据传递给解析器。当向解析器提供数据时,解析器将查找属性数据和音频数据包,当数据准备好时,将调用AudioFileStream_PropertyListenerProc和AudioFileStream_PacketsProc回调函数来处理数据。实际提供的数据量至少多于一个包的音频文件数据,但最好一次提供几个包到几秒钟的数据。

extern OSStatus
AudioFileStreamParseBytes(  
                                AudioFileStreamID                   inAudioFileStream,
                                UInt32                                  inDataByteSize,
                                const void * __nullable         inData,
                                AudioFileStreamParseFlags       inFlags);

inAudioFileStream:音频文件流解析器的 ID;

inDataByteSize:要解析的数据的字节数;

inData:要解析的数据;

inFlags:音频文件流标志。如果传递给解析器的最后一个数据存在不连续性,请设置该标志为:kAudioFileStreamParseFlag_Discontinuity

1.5.3 Seek

为数据流中的指定数据包提供字节偏移量。

extern OSStatus
AudioFileStreamSeek(    
                                AudioFileStreamID                  inAudioFileStream,
                                SInt64                                 inPacketOffset,
                                SInt64 *                               outDataByteOffset,
                                AudioFileStreamSeekFlags * ioFlags);

inAudioFileStream:音频文件流解析器的 ID;

inAbsolutePacketOffset:希望返回其字节偏移量的数据包文件开头的数据包数;

outAbsoluteByteOffset:在输出时,参数中指定其偏移量的数据包的绝对字节偏移量。对于不包含数据包表的音频文件格式,返回的偏移量可能是一个估计值;

ioFlags:在输出中,如果outAbsoluteByteOffset参数返回一个估计值,则该参数返回常量kAudioFileStreamSeekFlag_OffsetIsEstimated

1.5.4 获取属性

获取有关属性值的信息。

extern OSStatus
AudioFileStreamGetPropertyInfo( 
                                AudioFileStreamID                   inAudioFileStream,
                                AudioFileStreamPropertyID       inPropertyID,
                                UInt32 * __nullable               outPropertyDataSize,
                                Boolean * __nullable              outWritable);

inAudioFileStream:音频文件流解析器的 ID;

inPropertyID:需要其信息的音频文件流PropertyID

outPropertyDataSize:在输出时,指定属性的当前值的大小(以字节为单位)。

outWritable:在输出时,true如果可以写入属性,但目前没有可写的音频文件流属性。

1.5.5 获取属性值

检索指定属性的值。

extern OSStatus
AudioFileStreamGetProperty( 
                            AudioFileStreamID                     inAudioFileStream,
                            AudioFileStreamPropertyID     inPropertyID,
                            UInt32 *                                  ioPropertyDataSize,
                            void *                                    outPropertyData);

inAudioFileStream:音频文件流解析器的 ID;

inPropertyID:读取其值的音频文件流属性;

ioPropertyDataSize:参数中缓冲区的大小。可能通过调用AudioFileStreamGetPropertyInfo获取属性值的大小;

outPropertyData:输出指定属性的值。

1.5.6 设置属性

设置指定属性的值。目前音频文件流中,没有可以设置的属性。

extern OSStatus
AudioFileStreamSetProperty( 
                            AudioFileStreamID                     inAudioFileStream,
                            AudioFileStreamPropertyID     inPropertyID,
                            UInt32                                    inPropertyDataSize,
                            const void *                            inPropertyData);

inAudioFileStream:音频文件流解析器的 ID;

inPropertyID:要设置其值的音频文件流的PropertyID;

inPropertyDataSize:属性数据的大小(以字节为单位);

inPropertyData:属性数据。

2 实践与应用

为了验证AudioFileStream能力,这里仅通过 API,实现一个简化版本的 AudioFileParser,目标实现创建、解码、Seek、关闭能力。

2.1 主体框架

主体框架仅包含必要的定义,未实现任何功能,在下文,会针对每个功能补充必要的能力,完善 AudioFileParser。

@interface AudioFileParser () {
    AudioFileStreamID _audioFileStreamID;
}
/// 是否不连续
@property (nonatomic, assign) BOOL discontinuous;
/// 解析出来的packets
@property (nonatomic, strong) NSMutableArray *packets;
/// 音频数据在文件中的偏移
@property (nonatomic, assign) SInt64 dataOffset;
/// 已读数据在数据源文件中的偏移
@property (nonatomic, assign) SInt64 fileReadOffset;
/// 文件头解析完毕
@property (nonatomic, assign) BOOL readyToProducePackets;
@end
  
static void KSKitAudioFileStreamPropertyListener(void *inClientData,AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *inFlags) {
    AudioFileParser *parser = (__bridge AudioFileParser *)inClientData;
    [parser handleAudioFileStreamProperty:inPropertyID];
}

static void KSKitAudioFileStreamPacketCallBack(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescrrptions) {
    AudioFileParser *parser = (__bridge AudioFileParser *)inClientData;
    [parser handleAudioFileStreamPackets:inInputData
                           numberOfBytes:inNumberBytes
                         numberOfPackets:inNumberPackets
                       packetDescription:inPacketDescrrptions];
}

@implementation AudioFileParser
/// 初始化
- (instancetype)init {
    if (self = [super init]) {
    }
    return self;
}
/// 解析数据
- (BOOL)parse:(NSData *)data error:(NSError **)error {
}
/// 音频文件解析器Seek
- (BOOL)seek:(UInt32)packetCount error:(NSError **)error {
}
/// 关闭解析器
- (void)close {
}
/// 处理音频文件流的Property
/// @param propertyID Property 对应的 ID
- (void)handleAudioFileStreamProperty:(AudioFileStreamPropertyID)propertyID {
}
/// 处理音频文件流的 packets
/// @param packets 音频包数据
/// @param numberOfBytes 缓冲区中数据的字节数
/// @param numberOfPackets 缓冲区中音频数据的包数
/// @param packetDescriptions 描述数据的音频文件流数据包描述结构数组
- (void)handleAudioFileStreamPackets:(const void *)packets
                       numberOfBytes:(UInt32)numberOfBytes
                     numberOfPackets:(UInt32)numberOfPackets
                   packetDescription:(AudioStreamPacketDescription *)packetDescriptions {
    
}
@end

2.2 核心能力

2.2.1 初始化与关闭

  1. 在初始化AudioFileParser时,通过AudioFileStreamOpen创建音频文件流服务。readyToProducePackets 用来标识是否已经解析出音频文件头信息,discontinuous 用来标识是否连续,会在 Seek 实现中详细讲解。这里需要重点关注的是 KSKitAudioFileStreamPropertyListener 与 KSKitAudioFileStreamPacketCallBack,负责了音频数据回调与属性监听器回调。
- (instancetype)init {
    if (self = [super init]) {
        _readyToProducePackets = NO;
        _discontinuous = NO;
        _packets = [[NSMutableArray alloc] init];
        // inFileTypeHint 可以根据实际的传或者不指定
        OSStatus status = AudioFileStreamOpen((__bridge void *)self, KSKitAudioFileStreamPropertyListener, KSKitAudioFileStreamPacketCallBack, kAudioFileM4AType, &_audioFileStreamID);
        if (status != noErr) {
            return nil;
        }
    }
    return self;
}
  1. 音频文件流服务,需要手机关闭,通过AudioFileStreamClose关闭指定的解析器。
- (void)close {
    if (_audioFileStreamID) {
        AudioFileStreamClose(_audioFileStreamID);
        _audioFileStreamID = NULL;
    }
}

2.2.3 解析数据

初始化文件流解析器后,通过AudioFileStreamParseBytes对数据进行解码,数据由外部传递进来。我们通过 fileReadOffset 来标识,当前我们访问的数据在原始文件中的偏移。需要注意,在未解析到音频数据包前或者 Seek 之后,AudioFileStreamParseFlags 需要设置为 kAudioFileStreamParseFlag_Discontinuity

- (BOOL)parse:(NSData *)data error:(NSError **)error {
    BOOL bResult = YES;
    do {
        if (!data || !data.length) {
            bResult = NO;
            break;
        }
        // 已读偏移加上实际读取到的数据量,有可能读取到的数据要比要读的size少
        _fileReadOffset += data.length;
        OSStatus status;
        if (_discontinuous) {
            status = AudioFileStreamParseBytes(_audioFileStreamID, (UInt32)data.length, data.bytes, kAudioFileStreamParseFlag_Discontinuity);
        } else {
            status = AudioFileStreamParseBytes(_audioFileStreamID, (UInt32)data.length, data.bytes, 0);
        }
        if (status != noErr) {
            // handle error
            bResult = NO;
        }
    } while (NO);
    return bResult;
}

Note:AudioFileStream 本质上是对数据流的处理,并不特指是流媒体的资源,即使数据是本地文件,也是可以正常工作的,估这里命名为 AudioFileParser 而不是 AudioFileStreamParser。

2.2.3 获取音频文件信息

通过解析音频数据,解析器会解析并获取音频文件的头文件,会通过AudioFileStream_PropertyListenerProc回调(多次回调),这里重点关注关注:

  1. kAudioFileStreamProperty_ReadyToProducePackets 成功获取头信息会回调,回调后,discontinuous 与 readyToProducePackets 可以标识为 YES;
  2. kAudioFileStreamProperty_DataOffset 获取音频真实数据在音频文件的偏移值,Seek 时使用,这里注意上文说到的 fileReadOffset 原始数据偏移的区别
/// 处理音频文件流的Property
/// @param propertyID Property 对应的 ID
- (void)handleAudioFileStreamProperty:(AudioFileStreamPropertyID)propertyID {
    if (propertyID == kAudioFileStreamProperty_ReadyToProducePackets) {
        // 成功获取头部信息
        _readyToProducePackets = YES;
        _discontinuous = YES;
    } else if (propertyID == kAudioFileStreamProperty_DataOffset) {
        UInt32 offsetSize = sizeof(_dataOffset);
        // 获取音频真实数据在音频文件的偏移值
        OSStatus status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_DataOffset, &offsetSize, &_dataOffset);
        if(status != noErr) {
            NSLog(@"Parser get dataOffset error: %d", (int)status);
        }
    } 
}

2.2.3 处理音频数据包

在解析到音频文件信息之后,当解析器接收到足够的数据,会将解析到的音频数据包,通过AudioFileStream_PacketsProc回调出来,我们需要在该回调中,保存音频数据包的格式数据及音频包数据,提供给后继的转码器或者处理器使用。

- (void)handleAudioFileStreamPackets:(const void *)packets
                       numberOfBytes:(UInt32)numberOfBytes
                     numberOfPackets:(UInt32)numberOfPackets
                   packetDescription:(AudioStreamPacketDescription *)packetDescriptions {
    _discontinuous = NO;
    if (numberOfBytes == 0 || numberOfPackets == 0) {
        return;
    }
    
    BOOL deletePackDesc = NO;
    if (packetDescriptions == NULL) {
        deletePackDesc = YES;
        UInt32 packetSize = numberOfBytes / numberOfPackets;
        AudioStreamPacketDescription *descriptions = (AudioStreamPacketDescription *)malloc(sizeof(AudioStreamPacketDescription)*numberOfPackets);
        for (int i = 0; i < numberOfPackets; i++) {
            UInt32 packetOffset = packetSize * i;
            descriptions[i].mStartOffset  = packetOffset;
            descriptions[i].mVariableFramesInPacket = 0;
            if (i == numberOfPackets - 1) {
                descriptions[i].mDataByteSize = numberOfPackets-packetOffset;
            }else{
                descriptions[i].mDataByteSize = packetSize;
            }
        }
        packetDescriptions = descriptions;
    }
    
    for (int i = 0; i < numberOfPackets; i++) {
        SInt64 packetOffset = packetDescriptions[i].mStartOffset;
        AudioStreamPacketDescription aspd = packetDescriptions[i];
        UInt32 packetSize = aspd.mDataByteSize;
        // data该初始化方法底层默认copy一份数据
        NSData *data = [[NSData alloc] initWithBytes:packets+packetOffset length:packetSize];
        [_packets addObject:data];
    }
    
    if (deletePackDesc) {
        free(packetDescriptions);
        packetDescriptions = NULL;
    }
}

2.2.4 Seek 实现

AudioFileStream 中,Seek 本身只是获取音频文件在文件中偏移值,然后通过计算出在原始音频文件中偏移,通过读取新的数据包,实现 Seek 能力,需要注意的是在 Seek 之后,需要将 discontinuous 设置 YES,否则可能会遇到数据解码异常,同时需要把已经缓存的音频数据包清空,避免出现串数据而出现杂音。

- (BOOL)seek:(UInt32)packetCount error:(NSError **)error; {
    SInt64 outDataByteOffset;
    UInt32 ioFlags;
    OSStatus status = AudioFileStreamSeek(_audioFileStreamID, packetCount, &outDataByteOffset, &ioFlags);
    if ((status == noErr) && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)) {
        _fileReadOffset = _dataOffset + outDataByteOffset;
    } else {
        // handle error
        return NO;
    }
    _discontinuous = YES;
    // seek 后需要移除已经解析出来的包
    [_packets removeAllObjects];
    return YES;
}

Note:如果使用了转码器,Seek 之后,需要刷新其缓冲区。

2.3 小结

AudioFileParser 中仅实现简化版本的文件流解码器,比如音频文件格式、时长、总帧数。最大包大小等数据,需要读者去扩展其能力。这里仅介绍 AudioFileStream,实际应用中,AudioFileStream 很少单独应该,一般会结合 AudioConverter 、Audio Unit 或者更高级的音频 API 一起实现,实现解码器、转码器、处理器、播放器之间的联动。

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

推荐阅读更多精彩内容