AVFoundation-01音频播放与录制

概述

AVFoundation 是一个可以用来使用和创建基于时间的视听媒体数据的框架。AVFoundation 的构建考虑到了目前的硬件环境和应用程序,其设计过程高度依赖多线程机制。充分利用了多核硬件的优势并大量使用block和GCD机制,将复杂的计算机进程放到了后台线程运行。会自动提供硬件加速操作,确保在大部分设备上应用程序能以最佳性能运行。该框架就是针对64位处理器设计的,可以发挥64位处理器的所有优势。

iOS 媒体环境.png

数字媒体采样

对媒体内容进行数字化主要有两种方式。第一种称为时间采样,这种方法捕捉一个信号周期内的变化。第二种采样方式是空间采样,一般用在图片数字化和其它可视媒体内容数字化的过程。空间采样包含对一副图片在一定分辨率之下捕捉其亮度和色度,进而创建由该图片的像素点数据所构成的数字化结果。

音频采样

当我们记录一个声音时,一般会使用麦克风设备。麦克风设备是将机械能量(声波)转换成(电压信号)的转换设备。目前在用的麦克风种类很多,但是这里讨论的麦克风类型我们称为电动式麦克风。人类可以听到的音频范围是20Hz~20KHz。

电动式麦克风内部图.png

音频数字化的过程包含一个编码方法,称为线性脉冲编码调制(linear pulse-code modulation),比较常见的说法是Linear PCM或LPCM。这个过程采样或测量一个固定的音频信号,过程的周期率被称为采样率。如果不断提高采样的频率,我们就有可能以数字化方式准确表现原始信号的信息。鉴于硬件条件我们还不能复制出完全一样的效果,但是我们能找打一个采样率用于生成足够好的数字呈现效果。我们称其为奈奎斯特频率(Nyquist rate).Harry Nyquist是贝尔实验室的一名工程师,他精确地捕捉到了一个特定频率,该频率为需要采样对象的最高频率的两倍。除采样率外,数字音频采样的另一个重要方面是我们能够捕捉到什么精度的音频样本。振幅在线性坐标系中进行测量,所以会有Linear PCM这个术语。用于保存样本值的字节数定义了在线性维度上可行的离散度,同时这个信息也被称为音频的位元深度。为每个样本的整体量化分配过少的位结果信息会导致数字音频信号产生噪声和扭曲。使用位元深度为8的方法可以提供256个离散级别数据。对于一些音频资源来说,这个级别的采样率已经足够了,但对于大部分音频内容来说还不够高。CD音质的位元深度为16,可以达到65536个离散级别。专业级别的音频录制环境的位元深度可以达到24或更高。

AVAudioPlayer

音频播放器是很多应用程序的需求,AVAudioPlayer 让这一需求变得简单,它提供了一种简单地从文本或内存中播放视频的方法。它提供了Audio Queue Services 中所能找到的核心功能。除非你需要从网络流中播放音频、需要访问原始音频样本或者需要非常低的时延,否则AVAudioPlayer都能胜任。

  • 创建 AVAudioPlayer。可以通过NSData和本地音频文件的NSURL来创建。
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;
  • 播放控制。如果返回一个有效的播放实例,可以调用 - (BOOL)prepareToPlay; ,这样可以取得需要的音频硬件并预加载Audio Queue的缓冲区。当然,调用 - (void)play; 方法的时候会调用 - (BOOL)prepareToPlay;,如果优先调用 - (BOOL)prepareToPlay; 方法,播放的时候再调用 - (void)play; 方法,可以降低一定的延时。
@property float pan; 
@property float volume;
@property float rate;
@property NSInteger numberOfLoops;

- (BOOL)prepareToPlay;
- (void)play;
- (BOOL)playAtTime:(NSTimeInterval)time;
- (void)pause;
- (void)stop;
  • 音频计量。默认没有开启音频计量,一旦启用音频测量可以通过 - (void)updateMeters; 方法更新测量值,我们通过 - (float)peakPowerForChannel:(NSUInteger)channelNumber;- (float)averagePowerForChannel:(NSUInteger)channelNumber; 这两个函数得到db,平均分贝,峰值分贝。
@property(getter=isMeteringEnabled) BOOL meteringEnabled; 

- (void)updateMeters; 
- (float)peakPowerForChannel:(NSUInteger)channelNumber; 
- (float)averagePowerForChannel:(NSUInteger)channelNumber; 
  • 静音问题。当手机在铃声、静音间切换的时候,我们希望声音不会停止播放,可以加下面的代码。
NSError *error;
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]) {
    NSLog(@"setCategory error: %@", [error localizedDescription]);
    return;
}

if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
    NSLog(@"setActive error: %@", [error localizedDescription]);
    return;
}
  • 锁屏播放的问题。在 Info.plist 文件中,加入以下配置。
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>
  • 中断的问题。当电话呼入、闹铃响起等,在它们终止的时候,音频是不会如预期的恢复,我们可以监听相关通知来恢复。
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleInterruption:)
                                                     name:AVAudioSessionInterruptionNotification
                                                   object:nil];

- (void)handleInterruption:(NSNotification *)notice
{
    AVAudioSessionInterruptionType type = [notice.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [_musicPlayer pause];
    }else if (type == AVAudioSessionInterruptionTypeEnded) {
        [_musicPlayer play];
    }
}
  • 线路切换的问题。比如:在插上耳机时,我们希望声音从耳机内传出,但我们拔掉耳机的时候,我们希望的是音乐停止播放。
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleRouteChange:)
                                                     name:AVAudioSessionRouteChangeNotification
                                                   object:nil];

- (void)handleRouteChange:(NSNotification *)notice
{
    AVAudioSessionRouteChangeReason reason = [notice.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *preRoute = notice.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
        NSString *portType = [[preRoute.outputs firstObject] portType];
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [_musicPlayer pause];
        }
    }
}

AVAudioRecorder

AVAudioRecorder 也是构建于 Audio Queue Services 之上,是一个功能强大且简单易用的音频录制类。

  • 创建 AVAudioRecorder。需要提供本文件的NSURL 以及 相关录音参数设置。
- (nullable instancetype)initWithURL:(NSURL *)url settings:(NSDictionary<NSString *, id> *)settings error:(NSError **)outError;
- (nullable instancetype)initWithURL:(NSURL *)url format:(AVAudioFormat *)format error:(NSError **)outError;
  • 音频格式。AVFormatIDKey 定义录音文件的音频格式。下面的常量都是设备所支持的值。当你指定的格式和URL的文件类型不一致的时候会出现 The operation couldn’t be completed. (OSStatus error 1718449215.) 错误。
kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw
  • 采样率。AVSampleRateKey 定义录音文件的采样率。一般来说采样率越大,文件内容也越大。对于使用什么样的采样率,我们可以尽量使用标准的采样率。如 8000Hz、16000Hz、22050Hz、44100Hz。

  • 通道数。AVNumberOfChannelsKey 定义录音文件的通道数。一般使用默认值1,即单声道。

NSDictionary *setting = @{
                           AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                           AVSampleRateKey : @(44100),
                           AVNumberOfChannelsKey : @(1),
                           AVLinearPCMBitDepthKey : @(16),
                           AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                          };
  • 录音控制。录音的时候也可以先调用 - (BOOL)prepareToPlay; 真正录制的时候再调用 - (void) record; 方法,可以降低一定的延时。
- (BOOL)prepareToRecord; 

- (BOOL)record;
- (BOOL)recordAtTime:(NSTimeInterval)time;
- (BOOL)recordForDuration:(NSTimeInterval) duration;
- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration;

- (void)pause;
- (void)stop;

- (BOOL)deleteRecording;
  • 音频计量。默认没有开启音频计量,一旦启用音频测量可以通过 - (void)updateMeters; 方法更新测量值,我们通过 - (float)peakPowerForChannel:(NSUInteger)channelNumber;- (float)averagePowerForChannel:(NSUInteger)channelNumber; 这两个函数得到db,平均分贝,峰值分贝。
@property(getter=isMeteringEnabled) BOOL meteringEnabled; 
- (void)updateMeters; 

- (float)peakPowerForChannel:(NSUInteger)channelNumber; 
- (float)averagePowerForChannel:(NSUInteger)channelNumber; 

音频播放实例

1、新建 QMAudioController 播放控制类。

//
//  QMAudioController.m
//  AVFoundation
//
//  Created by mac on 17/6/20.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMAudioController.h"
#import "QMMeterTable.h"

@interface QMAudioController ()
@property (nonatomic, strong) QMMeterTable *meterTable;
@end

@implementation QMAudioController

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (instancetype)initWithContentsOfURL:(NSURL *)url
{
    if (url && (self = [super init])) {
        NSError *error = nil;
        _musicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
            return nil;
        }
       
        _musicPlayer.volume = 0.5f;
        _musicPlayer.pan = 0.0f;
        _musicPlayer.rate = 1.0f;
        _musicPlayer.numberOfLoops = -1;
        _musicPlayer.meteringEnabled = YES;
        [_musicPlayer prepareToPlay];
        
        _meterTable = [[QMMeterTable alloc] init];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleInterruption:)
                                                     name:AVAudioSessionInterruptionNotification
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleRouteChange:)
                                                     name:AVAudioSessionRouteChangeNotification
                                                   object:nil];
        
        return self;
    }
    
    return nil;
}

- (void)play
{
    NSError *error;
    if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]) {
        NSLog(@"setCategory error: %@", [error localizedDescription]);
        return;
    }

    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
        NSLog(@"setActive error: %@", [error localizedDescription]);
        return;
    }
    
    if (![_musicPlayer isPlaying]) {
        [_musicPlayer play];
    }
}

- (BOOL)playAtTime:(NSTimeInterval)time
{
    return [_musicPlayer playAtTime:time];
}

- (void)pause
{
    [_musicPlayer pause];
}

- (void)stop
{
    if ([_musicPlayer isPlaying]) {
        [_musicPlayer stop];
    }
}

- (BOOL)isPlaying
{
    return [_musicPlayer isPlaying];
}

- (void)updateMeters
{
    [_musicPlayer updateMeters];
}

- (float)peakValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_musicPlayer peakPowerForChannel:channelNumber]];
}

- (float)averageValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_musicPlayer averagePowerForChannel:channelNumber]];
}

#pragma mark - Notification
- (void)handleInterruption:(NSNotification *)notice
{
    AVAudioSessionInterruptionType type = [notice.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [_musicPlayer pause];
    }else if (type == AVAudioSessionInterruptionTypeEnded) {
        [_musicPlayer play];
    }
}

- (void)handleRouteChange:(NSNotification *)notice
{
    AVAudioSessionRouteChangeReason reason = [notice.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *preRoute = notice.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
        NSString *portType = [[preRoute.outputs firstObject] portType];
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [_musicPlayer pause];
        }
    }
}

@end

2、新建QMMeterTable音频计量转化类。我们将前面得到的分贝值的结果转化我线性的0到1形式,这需要一个转化的方式,如果我们把一次计算的结果记录下来,那么下次的计算就不需要了,因此提供下面的一个方法。

//
//  QMMeterTable.m
//  AVFoundation
//
//  Created by mac on 17/6/21.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMMeterTable.h"

#define MIN_DB          -60.0f
#define TABLE_SIZE      300


@interface QMMeterTable ()
{
    float                _scaleFactor;
    NSMutableArray      *_meterTable;
}
@end

@implementation QMMeterTable

static float dbToAmp(float dB)
{
    return powf(10.0f, 0.05f * dB);
}

- (id)init
{
    if (self = [super init]) {
        float dbResolution = MIN_DB / (TABLE_SIZE - 1);
        
        _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
        _scaleFactor = 1.0f / dbResolution;
        
        float minAmp = dbToAmp(MIN_DB);
        float ampRange = 1.0 - minAmp;
        float invAmpRange = 1.0 / ampRange;
        
        for (int i = 0; i < TABLE_SIZE; i++) {
            float decibels = i * dbResolution;
            float amp = dbToAmp(decibels);
            float adjAmp = (amp - minAmp) * invAmpRange;
            _meterTable[i] = @(adjAmp);
        }
    }
    return self;
}

- (float)valueForPower:(float)power
{
    if (power < MIN_DB) {
        return 0.0f;
    } else if (power >= 0.0f) {
        return 1.0f;
    } else {
        int index = (int) (power * _scaleFactor);
        return [_meterTable[index] floatValue];
    }
}
@end

3、使用播放控制类。每秒刷新12次去更新和获取音频计量。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _slider.transform = CGAffineTransformMakeRotation(-M_PI_2);
    
    NSURL *musicURL = [[NSBundle mainBundle] URLForResource:@"1" withExtension:@"mp3"];
    _musicPlayer = [[QMAudioController alloc] initWithContentsOfURL:musicURL];
    
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter:)];
    self.displayLink.frameInterval = 5;
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)updateMeter:(CADisplayLink *)link
{
        [_musicPlayer updateMeters];
        _slider.value = [_musicPlayer averageValueForChannel:0];
}

音频录制实例

1、新建 QMAudioRecorderController 音频录制管理类。

//
//  QMAudioRecorderController.m
//  AVFoundation
//
//  Created by mac on 17/6/21.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMAudioRecorderController.h"
#import "QMMeterTable.h"

@interface QMAudioRecorderController()
@property (nonatomic, strong) QMMeterTable *meterTable;
@end

@implementation QMAudioRecorderController

- (instancetype)initWithContentsOfURL:(NSURL *)url
{
    if (url && (self = [super init])) {
        NSDictionary *setting = @{
                               AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                               AVSampleRateKey : @(44100),
                               AVNumberOfChannelsKey : @(1),
                               AVLinearPCMBitDepthKey : @(16),
                               AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                               };
        
        NSError *error;
        _audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
            return nil;
        }
        
        _meterTable = [[QMMeterTable alloc] init];
        _audioRecorder.meteringEnabled = YES;
        _audioRecorder.delegate = self;
        [_audioRecorder prepareToRecord];
        
        return self;
    }
    
    return nil;
}

- (BOOL)isRecording
{
    return [_audioRecorder isRecording];
}

- (BOOL)record
{
    NSError *error;
    if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
        NSLog(@"setCategory error: %@", [error localizedDescription]);
        return NO;
    }
    
    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
        NSLog(@"setActive error: %@", [error localizedDescription]);
        return NO;
    }
    
    return [_audioRecorder record];
}

- (BOOL)recordAtTime:(NSTimeInterval)time
{
    return [_audioRecorder recordAtTime:time];
}

- (BOOL)recordForDuration:(NSTimeInterval) duration
{
    return [_audioRecorder recordForDuration:duration];
}

- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration
{
    return [_audioRecorder recordAtTime:time forDuration:duration];
}

- (void)pause
{
    [_audioRecorder pause];
}

- (void)stop
{
    [_audioRecorder stop];
}

- (BOOL)deleteRecording
{
    return [_audioRecorder deleteRecording];
}

- (void)updateMeters
{
    [_audioRecorder updateMeters];
}

- (float)peakValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_audioRecorder peakPowerForChannel:channelNumber]];
}

- (float)averageValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_audioRecorder averagePowerForChannel:channelNumber]];
}

#pragma mark - AVAudioRecorderDelegate
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
    if (self.finishCallback) {
        self.finishCallback(flag, nil);
    }
}

- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError * __nullable)error;
{
    NSLog(@"RecorderEncodeError error:%@", [error localizedDescription]);
    
    if (self.finishCallback) {
        self.finishCallback(NO, error);
    }
}

@end

2、新建QMMeterTable音频计量转化类。我们将前面得到的分贝值的结果转化我线性的0到1形式,这需要一个转化的方式,如果我们把一次计算的结果记录下来,那么下次的计算就不需要了,因此提供下面的一个方法。

//
//  QMMeterTable.m
//  AVFoundation
//
//  Created by mac on 17/6/21.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMMeterTable.h"

#define MIN_DB          -60.0f
#define TABLE_SIZE      300


@interface QMMeterTable ()
{
    float                _scaleFactor;
    NSMutableArray      *_meterTable;
}
@end

@implementation QMMeterTable

static float dbToAmp(float dB)
{
    return powf(10.0f, 0.05f * dB);
}

- (id)init
{
    if (self = [super init]) {
        float dbResolution = MIN_DB / (TABLE_SIZE - 1);
        
        _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
        _scaleFactor = 1.0f / dbResolution;
        
        float minAmp = dbToAmp(MIN_DB);
        float ampRange = 1.0 - minAmp;
        float invAmpRange = 1.0 / ampRange;
        
        for (int i = 0; i < TABLE_SIZE; i++) {
            float decibels = i * dbResolution;
            float amp = dbToAmp(decibels);
            float adjAmp = (amp - minAmp) * invAmpRange;
            _meterTable[i] = @(adjAmp);
        }
    }
    return self;
}

- (float)valueForPower:(float)power
{
    if (power < MIN_DB) {
        return 0.0f;
    } else if (power >= 0.0f) {
        return 1.0f;
    } else {
        int index = (int) (power * _scaleFactor);
        return [_meterTable[index] floatValue];
    }
}

@end

3、进行音频录制。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _slider.transform = CGAffineTransformMakeRotation(-M_PI_2);
    
    NSURL *recordFileURL = [NSURL fileURLWithPath:kDocumentPath(@"1.aac")];
    _audioRecorder = [[QMAudioRecorderController alloc] initWithContentsOfURL:recordFileURL];
    
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter:)];
    self.displayLink.frameInterval = 5;
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

#pragma mark - Timer
- (void)updateMeter:(CADisplayLink *)link
{
     [_audioRecorder updateMeters];
     _slider.value = [_audioRecorder averageValueForChannel:0];
}

参考

AVFoundation开发秘籍:实践掌握iOS & OSX应用的视听处理技术

源码地址:AVFoundation开发 https://github.com/QinminiOS/AVFoundation

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

推荐阅读更多精彩内容