LCWechat -- 封裝錄音與播放

簡略描述: AVAudioRecorder 與 AVAudioPlayer(只能播放本地音頻, 遠程音頻使用AVPlayer)都是使用代理的方法判斷是否停止, 錄音的時候需要設置recordSetting, 字典的形式存儲了幾種錄音需要的格式,例如:采樣率, 通道數,錄音質量等.還有一個AVAudioSession 是一個單例 [AVAudioSession sharedInstance] 在錄音和播放音頻的需要設置一下其模式AVAudioSessionCategoryOptions, 每個枚舉類型都有其對應的應用場景.

摘抄:
在獲得一個AVAudioSession類的實例后,你就能通過調用音頻會話對象的setCategory:error:實例方法,來從IOS應用可用的不同類別中作出選擇。下面列出了可供使用的音頻會話類別:
AVAudioSessionCategorySoloAmbient

這個類別非常像AVAudioSessionCategoryAmbient類別,除了會停止其他程序的音頻回放,比如iPod程序。當設備被設置為靜音模式,你的音頻回放將會停止。

AVAudioSessionCategoryRecord
這會停止其他應用的聲音(比如iPod)并讓你的應用也不能初始化音頻回放(比如AVAudioPlayer)。在這種模式下,你只能進行錄音。使用這個類別,調用AVAudioPlayer的prepareToPlay會返回YES,但是調用play方法將返回NO。主UI界面會照常工作。這時,即使你的設備屏幕被用戶鎖定了,應用的錄音仍會繼續。

AVAudioSessionCategoryPlayback
這個類別會靜止其他應用的音頻回放(比如iPod應用的音頻回放)。你可以使用AVAudioPlayer的prepareToPlay和play方法,在你的應用中播放聲音。主UI界面會照常工作。這時,即使屏幕被鎖定或者設備為靜音模式,音頻回放都會繼續。

AVAudioSessionCategoryPlayAndRecord
這個類別允許你的應用中同時進行聲音的播放和錄制。當你的聲音錄制或播放開始后,其他應用的聲音播放將會停止。主UI界面會照常工作。這時,即使屏幕被鎖定或者設備為靜音模式,音頻回放和錄制都會繼續。

AVAudioSessionCategoryAudioProcessing
這個類別用于應用中進行音頻處理的情形,而不是音頻回放或錄制。設置了這種模式,你在應用中就不能播放和錄制任何聲音。調用AVAPlayer的prepareToPlay和play方法都將返回NO。其他應用的音頻回放,比如iPod,也會在此模式下停止。

AVAudioSessionCategoryAmbient
這個類別不會停止其他應用的聲音,相反,它允許你的音頻播放于其他應用的聲音之上,比如iPod。你的應用的主UI縣城會工作正常。調用AVAPlayer的prepareToPlay和play方法都將返回YES。當用戶鎖屏時,你的應用將停止所有正在回放的音頻。僅當你的應用是唯一播放該音頻文件的應用時,靜音模式將停止你程序的音頻回放。如果正當iPod播放一手歌時,你開始播放音頻,將設備設為靜音模式并不能停止你的音頻回放。

思路:

1.創建一個AudioManager統一管理audioRecord和audioPlay
2.先分別實現audioRecord和audioPlay
3.基于audioRecord和audioPlay 在AudioManager 統一處理
4.就是一個封裝, 花點時間 就能看懂

.h文件
#import <Foundation/Foundation.h>

@interface LCAudioManager : NSObject

+ (instancetype)manager;

#pragma mark - LCAudioRecord
// 判斷麥克風是否可用
- (BOOL)checkMicrophoneAvailability;
 
/**
 *  開始錄音
 *
 */
- (void)startRecordingWithFileName:(NSString *)fileName
                             completion:(void(^)(NSError *error))completion;

/**
 *  停止錄音
 *
 */
- (void)stopRecordingWithCompletion:(void(^)(NSString *recordPath,
                                                 NSInteger aDuration,
                                                 NSError *error))completion;
/**
 *  取消錄音
 */
- (void)cancelRecording;

/**
 *  當前是否正在錄音
 *
 */
- (BOOL)isRecording;


#pragma mark - LCAudioPlay
/**
 *  播放音頻
 *
 */
- (void)playingWithRecordPath:(NSString *)recordPath
                  completion:(void(^)(NSError *error))completion;

/**
 *  停止播放
 *
 */
- (void)stopPlaying;

/**
 *  當前是否正在播放
 *
 */
-(BOOL)isPlaying;
@end

.m文件
// 當前代碼是將 錄音轉換為mp3, 如不需轉換 直接注釋此代碼, 并設置一下相應的錄音格式,例如通道數,采樣率等
//BOOL convertResult = [self convertWAV:recordPath toMP3:mp3FilePath];
#import "LCAudioManager.h"
#import "LCAudioPlay.h"
#import "LCAudioRecord.h"
#import <AVFoundation/AVFoundation.h>
#import "lame.h"

#define audioRecordDurationTooShort -100
#define audioRecordStoping -101
#define audioRecordNotStarted -102
#define audioRecordConvertionFailure -103
#define audioRecordPathNotFound -104

#define recordMinDuration 1.0

typedef NS_ENUM(NSInteger, audioSession){
    audioSessionDefault = 0,
    audioSessionAudioRecord = 1,
    audioSessionPlay = 2
};

@interface LCAudioManager ()

@property (strong, nonatomic) NSDate *recordStartDate;
@property (strong, nonatomic) NSDate *recordEndDate;
@property (copy, nonatomic) NSString *audioSessionCategory;
@property (assign, nonatomic) BOOL currentActive;

@end

@implementation LCAudioManager

+ (instancetype)manager
{
    static LCAudioManager *audioManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        audioManager = [[self alloc] init];
    });
    
    return audioManager;
}


- (BOOL)checkMicrophoneAvailability{
    __block BOOL open = NO;
    AVAudioSession *session = [AVAudioSession sharedInstance];
    if ([session respondsToSelector:@selector(requestRecordPermission:)]) {
        [session performSelector:@selector(requestRecordPermission:) withObject:^(BOOL status) {
            open = status;
        }];
    } else {
        open = YES;
    }
    
    return open;
}

#pragma mark - LCAudioRecord
- (void)startRecordingWithFileName:(NSString *)fileName completion:(void (^)(NSError *))completion
{
    if ([self isRecording]) {
        [self cancelRecording];
        if (completion) completion([NSError errorWithDomain:NSLocalizedString(@"LCAudio.recordStop", @"停止當前錄音") code:audioRecordStoping userInfo:nil]);
        
        return;
    }
    
    if (!fileName || fileName.length == 0) {
        if (completion) completion([NSError errorWithDomain:NSLocalizedString(@"LCAudio.recordPathNotFound", @"尚未找到文件") code:audioRecordPathNotFound userInfo:nil]);
        return;
    }
    
    [self setCategory:audioSessionAudioRecord isActive:YES];
    [[LCAudioRecord sharedInstance] startRecordingWithRecordPath:[self recordPathWithfileName:fileName] completion:completion];
    
    self.recordStartDate = [NSDate date];
}

- (void)stopRecordingWithCompletion:(void (^)(NSString *, NSInteger, NSError *))completion
{
    if (![self isRecording]) {
        if (completion) completion(nil, 0, [NSError errorWithDomain:NSLocalizedString(@"LCAudio.recordNotStart", @"未有錄音") code:audioRecordNotStarted userInfo:nil]);
        
        return;
    }
    
    self.recordEndDate = [NSDate date];
    __weak typeof(self) weakSelf = self;
    NSTimeInterval duration = [self.recordEndDate timeIntervalSinceDate:self.recordStartDate];
    if (duration < recordMinDuration) {
        if (completion) completion(nil, 0, [NSError errorWithDomain:NSLocalizedString(@"LCAudio.recordTimeTooShort", @"錄音小于1秒") code:audioRecordDurationTooShort userInfo:nil]);
        
        
        [[LCAudioRecord sharedInstance] stopRecordingWithCompletion:^(NSString *recordPath) {
            [weakSelf setCategory:audioSessionDefault isActive:NO];
        }];
        
        return;
    }
    
    [[LCAudioRecord sharedInstance] stopRecordingWithCompletion:^(NSString *recordPath) {
        if (completion) {
            NSString *mp3FilePath = [self MP3FilePath:recordPath];
            BOOL convertResult = [self convertWAV:recordPath toMP3:mp3FilePath];
            if (convertResult) {
                // 刪除錄的wav
                [[NSFileManager defaultManager] removeItemAtPath:recordPath error:nil];
            }
            
            completion(mp3FilePath,(NSInteger)duration, nil);
        }
        
        [weakSelf setCategory:audioSessionDefault isActive:NO];
    }];
}

- (void)cancelRecording
{
    [[LCAudioRecord sharedInstance] cancelRecording];
}

- (BOOL)isRecording
{
    return [[LCAudioRecord sharedInstance] isRecording];
}

#pragma mark - LCAudioPlay

- (void)playingWithRecordPath:(NSString *)recordPath completion:(void (^)(NSError *))completion
{
    if ([self isPlaying]) [self stopPlaying];
    
    [self setCategory:audioSessionPlay isActive:YES];
    
    NSString *mp3FilePath = [self MP3FilePath:recordPath];
    
    if (![[NSFileManager defaultManager] fileExistsAtPath:mp3FilePath]) { // 如果沒有轉化成功,嘗試再次轉換
        BOOL convertResult = [self convertWAV:recordPath toMP3:mp3FilePath];
        if (convertResult) {
            // 刪除錄的wav
            [[NSFileManager defaultManager] removeItemAtPath:recordPath error:nil];
        } else {
            if (completion) completion([NSError errorWithDomain:NSLocalizedString(@"LCAudio.recordConvertionFailure", @"轉換文件失敗") code:audioRecordConvertionFailure userInfo:nil]);
            
            return;
        }
    }
    
    [[LCAudioPlay sharedInstance] playingWithPath:[self MP3FilePath:recordPath] completion:^(NSError *error) {
        [self setCategory:audioSessionDefault isActive:NO];
        if (completion) completion(error);
    }];
}

- (void)stopPlaying
{
    [[LCAudioPlay sharedInstance] stopPlaying];
    [self setCategory:audioSessionDefault isActive:NO];
}

- (BOOL)isPlaying
{
    return [[LCAudioPlay sharedInstance] isPlaying];
}

#pragma mark - setCategory && setActive
- (void)setCategory:(audioSession)session isActive:(BOOL)active
{
    NSError *error = nil;
    NSString *category = nil;
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    
    switch (session) {
        case audioSessionAudioRecord:
            category = AVAudioSessionCategoryRecord;
            break;
        case audioSessionPlay:
            category = AVAudioSessionCategoryPlayback;
            break;
        default:
            category = AVAudioSessionCategoryAmbient;
            break;
    }
    
    if (![self.audioSessionCategory isEqualToString:category]) [audioSession setCategory:category error:nil];
    
    
    if (active != self.currentActive) {
        self.currentActive = active;
        BOOL success = [audioSession setActive:active withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
        if (!success || error) return ;
    }
    
    self.audioSessionCategory = category;
    
}

#pragma mark - path
- (NSString *)recordPathWithfileName:(NSString *)fileName
{
    NSString *recordPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    recordPath = [NSString stringWithFormat:@"%@/records/",recordPath];
    recordPath = [recordPath stringByAppendingPathComponent:fileName];
    if(![[NSFileManager defaultManager] fileExistsAtPath:[recordPath stringByDeletingLastPathComponent]]){
        [[NSFileManager defaultManager] createDirectoryAtPath:[recordPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    return recordPath;
}

- (NSString *)MP3FilePath:(NSString *)aFilePath
{
    return [[aFilePath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mp3"];
}
#pragma mark - Convert
// 使用三方庫 lame // 需要將libmp3lame 拖進項目
- (BOOL)convertWAV:(NSString *)wavFilePath toMP3:(NSString *)mp3FilePath{
    BOOL ret = NO;
    BOOL isFileExists = [[NSFileManager defaultManager] fileExistsAtPath:wavFilePath];
    if (isFileExists) {
        int read, write;
        
        FILE *pcm = fopen([wavFilePath cStringUsingEncoding:1], "rb");   //source 被轉換的音頻文件位置
        fseek(pcm, 4*1024, SEEK_CUR);                                                   //skip file header
        FILE *mp3 = fopen([mp3FilePath  cStringUsingEncoding:1], "wb"); //output 輸出生成的Mp3文件位置
        
        const int PCM_SIZE = 8192;
        const int MP3_SIZE = 8192;
        short int pcm_buffer[PCM_SIZE*2];
        unsigned char mp3_buffer[MP3_SIZE];
        
        lame_t lame = lame_init();
        lame_set_in_samplerate(lame, 11025.0);
        lame_set_VBR(lame, vbr_default);
        lame_init_params(lame);
        
        do {
            read = fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
            if (read == 0)
                write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
            else
                write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
            
            fwrite(mp3_buffer, write, 1, mp3);
            
        } while (read != 0);
        
        lame_close(lame);
        fclose(mp3);
        fclose(pcm);
        isFileExists = [[NSFileManager defaultManager] fileExistsAtPath:mp3FilePath];
        if (isFileExists) {
            ret = YES;
        }
    }
    
    return ret;
}
@end

audioPlay

.h文件
#import <Foundation/Foundation.h>

@interface LCAudioPlay : NSObject

+ (instancetype)sharedInstance;

/**
 *  當前是否正在播放
 *
 */
- (BOOL)isPlaying;

/**
 *  播放音頻
 *
 */
- (void)playingWithPath:(NSString *)recordPath
                  completion:(void(^)(NSError *error))completion;

/**
 *  停止播放
 *
 */
- (void)stopPlaying;
@end

.m文件
#import "LCAudioPlay.h"
#import <AVFoundation/AVFoundation.h>

#define audioFileNotFound -106
#define audioPlayerInitFilure -107

@interface LCAudioPlay ()<AVAudioPlayerDelegate>

@property (strong, nonatomic) AVAudioPlayer *player;
@property (copy, nonatomic) void(^completion)(NSError *);

@end

@implementation LCAudioPlay
+ (instancetype)sharedInstance
{
    static LCAudioPlay *player = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        player = [[self alloc] init];
    });
    
    return player;
}

- (void)playingWithPath:(NSString *)recordPath completion:(void (^)(NSError *))completion
{
    self.completion = completion;
    
    NSError *error = nil;
    
    if (![[NSFileManager defaultManager] fileExistsAtPath:recordPath]) {
        error = [NSError errorWithDomain:NSLocalizedString(@"LCAudio.fileNotFound", @"未找到文件") code:audioFileNotFound userInfo:nil];
        
        if (self.completion) self.completion(error);
        
        self.completion = nil;
        
        return;
    }
    
    NSURL *mp3URL = [NSURL fileURLWithPath:recordPath];
    
    self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:mp3URL error:&error];
    
    if (!self.player || error) {
        error = [NSError errorWithDomain:NSLocalizedString(@"LCAudio.audioPlayerInitFilure", @"初始化播放器失敗") code:audioPlayerInitFilure userInfo:nil];
        
        self.player = nil;
        
        if (self.completion) self.completion(error);
        
        self.completion = nil;
        
        return;
    }
    
    self.player.delegate = self;
    [self.player prepareToPlay];
    [self.player play];
    
}

- (BOOL)isPlaying
{
    return !!self.player;
}

- (void)stopPlaying
{
    if (self.player) {
        self.player.delegate = nil;
        [self.player stop];
        self.player = nil;
    }
    
    if (self.completion) self.completion = nil;
}

- (void)dealloc
{
    [self stopPlaying];
}
#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    if (self.completion) self.completion(nil);
    [self stopPlaying];
}

@end

audioRecord

.h文件
#import <Foundation/Foundation.h>

@interface LCAudioRecord : NSObject

+ (instancetype)sharedInstance;

/**
 *  開始錄音
 *
 */
- (void)startRecordingWithRecordPath:(NSString *)recordPath
                             completion:(void(^)(NSError *error))completion;

/**
 *  停止錄音
 *
 */
- (void)stopRecordingWithCompletion:(void(^)(NSString *recordPath))completion;

/**
 *  取消錄音
 */
- (void)cancelRecording;

/**
 *  當前是否正在錄音
 *
 */
- (BOOL)isRecording;
@end

.m文件
#import "LCAudioRecord.h"
#import <AVFoundation/AVFoundation.h>

#define recorderInitFailure -105

@interface LCAudioRecord ()<AVAudioRecorderDelegate>

@property (nonatomic, strong) AVAudioRecorder *recorder;
@property (nonatomic, strong) NSDictionary *recordSetting;
@property (copy, nonatomic) void(^completion)(NSString *recordPath);

@end

@implementation LCAudioRecord

+ (instancetype)sharedInstance
{
    static LCAudioRecord *audioRecord = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        audioRecord = [[self alloc] init];
    });
    
    return audioRecord;
}   

- (NSDictionary *)recordSetting
{
    if (!_recordSetting) { // 轉換為的MP3格式
        _recordSetting = [[NSDictionary alloc] initWithObjectsAndKeys:
                          [NSNumber numberWithFloat: 11025.0],AVSampleRateKey, //采樣率 44100.0
                          [NSNumber numberWithInt: kAudioFormatLinearPCM],AVFormatIDKey,
                          [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,//采樣位數 默認 16
                          [NSNumber numberWithInt: 2], AVNumberOfChannelsKey,//通道的數目
                          [NSNumber numberWithInt:AVAudioQualityMax], AVEncoderAudioQualityKey, // 錄音質量
                          nil];
    }
    
    return _recordSetting;
}

- (void)startRecordingWithRecordPath:(NSString *)recordPath completion:(void (^)(NSError *))completion
{
    NSError *error = nil;
    NSURL *wavURL = [NSURL fileURLWithPath:[[recordPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"wav"]];
    self.recorder = [[AVAudioRecorder alloc] initWithURL:wavURL settings:self.recordSetting error:&error];
    if (!self.recorder || error) {
        if (completion) {
            error = [NSError errorWithDomain:NSLocalizedString(@"LCAudio.recorderInitFailure", @"初始化失敗") code:recorderInitFailure userInfo:nil];
            completion(error);
        }
        self.recorder = nil;
        return;
    }
    
    self.recorder.meteringEnabled = YES;
    self.recorder.delegate = self;
    [self.recorder record];
    
    if (completion) completion(error);
    
}

- (void)stopRecordingWithCompletion:(void(^)(NSString *recordPath))completion
{
    self.completion = completion;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.recorder stop];
    });
}

- (BOOL)isRecording
{
    return !!self.recorder;
}

- (void)cancelRecording
{
    self.recorder.delegate = nil;
    if (self.recorder) [self.recorder stop];
    self.recorder = nil;
    
}

- (void)dealloc
{
    if (self.recorder) {
        self.recorder.delegate = nil;
        [self.recorder stop];
        [self.recorder deleteRecording];
        self.recorder = nil;
    }
}

- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
    NSString *recordPath = [[_recorder url] path];
    if (self.completion) {
        if (!flag) recordPath = nil;
        self.completion(recordPath);
    }
    
    self.recorder = nil;
    self.completion = nil;
}

@end

如何使用:

1.開始錄音
// 檢測麥克風是否可用
[LCAudioManager manager] checkMicrophoneAvailability]
[[LCAudioManager manager] startRecordingWithFileName:[NSString recordFileName] completion:nil];

2.結束錄音
[[LCAudioManager manager] stopRecordingWithCompletion:^(NSString *recordPath, NSInteger aDuration, NSError *error) {
        if (aDuration < 1) { 
            [MBProgressHUD showError:@"錄音時間過短"];
            return ;
        }
        if (!error) { // 錄音成功
          // 執行下一步計劃
        }
    }];

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容