美文网首页
推送语音播报--简单归纳

推送语音播报--简单归纳

作者: Gxdy | 来源:发表于2020-12-07 20:10 被阅读0次

    前言:在iOS 12.01之后AVSpeechSynthesisVoice(文字转语音)已经不可用,所以只能使用修改推送中的sound属性来实现播报

    可供参考的实现方法:
    1. 将所有需要的播报场景音频文件放入工程main bundle,然后根据需要播报对影文件。
    • 该方法适用于播报场景少的情况
    1. 使用三方离线语音合成或在线合成sdk(如百度、讯飞等),对要合成的文本进行转换,然后设置到sound属性中
    • 需要开App Groups,将合成的音频文件保存到Groups对应的Library/Sounds目录下
    • 需付费
    1. 由后台合成,然后根据通过推送进行下载,然后设置到sound上
    • 需要开App Groups,将合成的音频文件保存到Groups对应的Library/Sounds目录下
    • 参考文章
    1. 本地音频文件组合(如将‘您的的支付宝到账100.65元’,由‘您的的支付宝到账’、‘98’和‘点六五元’三个文件组合而成)
    • 需要开App Groups,将合成的音频文件保存到Groups对应的Library/Sounds目录下
    • 需要设置通知应用扩展程序的在info.plist使用后台模式, 不设置回报如下错误
    • ⚠️上面这条是苹果不允许的,所以这只适合不走App Store的情况
    • 参考
    // 错误
    Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-16980), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x10550bba0 {Error Domain=NSOSStatusErrorDomain Code=-16980 "(null)"}}
    
    • 3和4的参考代码
    #import "NotificationService.h"
    #import <AVFoundation/AVFoundation.h>
    
    /// 播报类型
    typedef enum : NSUInteger {
        ///  默认,不播报
        TLSpeakerTypeNone = 0,
        /// 收到一笔预订订单,请及时处理
        TLSpeakerTypeOrderOfBook,
        /// 收到一笔外卖订单,请及时处理
        TLSpeakerTypeOrderOfTakeaway,
        /// 收到一笔报餐订单,请及时处理
        TLSpeakerTypeOrderOfReport,
        /// 收到微信付款**元
        TLSpeakerTypePaymentOfWechat,
        /// 收到支付宝付款**元
        TLSpeakerTypePaymentOfAli,
    } TLSpeakerType;
    
    
    @interface NotificationService ()
    /// 当前播报类型
    @property(nonatomic, assign) TLSpeakerType type;
    /// 通知内容
    @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
    /// 通知处理完后的回调
    @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    /// 下载task
    @property(nonatomic, strong) NSURLSessionDownloadTask *task;
    @end
    
    @implementation NotificationService
    
    /// 收到通知后的处理
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
        
        NSDictionary *userInfo = self.bestAttemptContent.userInfo;
        
        NSInteger type = [userInfo[@"type"] integerValue];
        CGFloat value = 0.f;
        if (type <= TLSpeakerTypePaymentOfAli) {
            self.type = type;
            value = [userInfo[@"amount"] floatValue];
        }
        [self setNotificationSoundWithType:self.type
                                     value:value
                       notificationContent:self.bestAttemptContent
                notificationContentHandler:contentHandler];
        
        //    [self loadWavWithUrl:@"https://test.iyouxin.com/order_pic/abcd.mp3"];
    }
    
    /// 处理即将过期,进行默认处理
    - (void)serviceExtensionTimeWillExpire {
        NSString *soundName = nil;
        switch (self.type) {
            case TLSpeakerTypeOrderOfBook:
                soundName = @"order_book.mp3";
                break;
            case TLSpeakerTypeOrderOfTakeaway:
                soundName = @"order_takeaway.mp3";
                break;
            case TLSpeakerTypeOrderOfReport:
                soundName = @"order_report.mp3";
                break;
            case TLSpeakerTypePaymentOfWechat:
                soundName = @"wx_normal.mp3";
                break;
            case TLSpeakerTypePaymentOfAli:
                soundName = @"ali_normal.mp3";
                break;
            default:
                break;
        }
        if (soundName) {
            self.bestAttemptContent.sound = [UNNotificationSound soundNamed:soundName];
        }
        self.contentHandler(self.bestAttemptContent);
    }
    
    /// 最大播报金额
    #define kMaxValue 100
    
    /// 通知拦截处理
    - (void)setNotificationSoundWithType:(TLSpeakerType)type
                                   value:(CGFloat )value
                     notificationContent:(UNMutableNotificationContent *)notificationContent
              notificationContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        
        if (type < TLSpeakerTypePaymentOfWechat) {
            [self serviceExtensionTimeWillExpire];
            return;
        }
        
        // > kMaxValue
        if (value > kMaxValue) {
            NSString *soundName = type == TLSpeakerTypePaymentOfWechat ? @"wx_normal.mp3" : @"ali_normal.mp3";
            if (soundName) {
                notificationContent.sound = [UNNotificationSound soundNamed:soundName];
            }
            self.contentHandler(notificationContent);
            return;
        }
        
        // <= kMaxValue
        NSArray *sourceURLs = [self paymentSoundSourcesWithType:type value:value];
        NSString *path = self.filePath;
        NSString *soundName = @"synthetic_sound.m4a";
        NSString *toPath = [path stringByAppendingPathComponent:[NSString stringWithFormat:@"%@", soundName]];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        if ([fileManager fileExistsAtPath:toPath]) {
            // 移除旧的文件
            [fileManager removeItemAtPath:toPath error:nil];
        }
        
        NSURL *outputURL = [NSURL fileURLWithPath:toPath];
        [self sourceURLs:sourceURLs composeToURL:outputURL completed:^(NSError *error) {
             if (error) {
                 [self serviceExtensionTimeWillExpire];
             }else{
                 notificationContent.sound = [UNNotificationSound soundNamed:soundName];
                 self.contentHandler(notificationContent);
             }
        }];
    }
    
    // 获取小于kMaxValue的付款音频文件URL集合,为合并做准备
    - (NSArray <NSURL *>*)paymentSoundSourcesWithType:(TLSpeakerType)type value:(CGFloat)value {
        NSMutableArray *urls = [NSMutableArray array];
        NSString *fileName1 = type == TLSpeakerTypePaymentOfWechat ? @"payment_wechat" : @"payment_ali";
        NSURL *url = [self urlWithFileName:fileName1];
        if (url) {
            [urls addObject:url];
        }
        
        NSInteger num = @(value).integerValue;
        NSString *fileName2 = @(num).stringValue;
        url = [self urlWithFileName:fileName2];
        if (url) {
            [urls addObject:url];
        }
        
        NSString *number = [self twoDecimalsWithNum:@(value).stringValue];
        NSInteger decimals = [[number componentsSeparatedByString:@"."].lastObject integerValue];
        NSString *fileName3 = decimals <= 0 ? @"元" : [NSString stringWithFormat:@"点%02zi元", decimals];
        url = [self urlWithFileName:fileName3];
        if (url) {
            [urls addObject:url];
        }
        return urls;
    }
    
    // MARK: - 合并音频文件
    /// 合并音频文件
    /// @param sourceURLs 需要合并的多个音频文件
    /// @param outputURL  合并后音频文件的临时存放地址
    /// 注意:导出的文件是:m4a格式的.
    - (void)sourceURLs:(NSArray *)sourceURLs
          composeToURL:(NSURL *)outputURL
             completed:(void (^)(NSError *error))completed {
    
        if (sourceURLs.count < 1) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSString *domain = @"合并音频文件";
                NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"源文件不足两个无需合并"};
                NSError *error = [NSError errorWithDomain:domain code:-101 userInfo:userInfo];
                completed(error);
            });
            return;
        }
    
        // 合并所有的录音文件
        AVMutableComposition *mixComposition = [AVMutableComposition composition];
    
        // 音频插入的开始时间
        CMTime beginTime = kCMTimeZero;
        // 获取音频合并音轨
        AVMutableCompositionTrack *audioTrack = nil;
        audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio
                                                 preferredTrackID:kCMPersistentTrackID_Invalid];
    
        // 用于记录错误的对象
        NSError *error = nil;
        CGFloat i = 1;
        for (NSURL *sourceURL in sourceURLs) {
            // 音频文件资源
            AVURLAsset  *audioAsset = [[AVURLAsset alloc] initWithURL:sourceURL options:nil];
            // 需要合并的音频文件的区间
            CMTimeValue value = audioAsset.duration.value * i;
            CMTimeScale timescale = audioAsset.duration.timescale;
            CMTimeRange timeRange = CMTimeRangeMake(CMTimeMake(value * (1 - i) * 0.5 , timescale),
                                                    CMTimeMake(value , timescale));
            i = 0.8;
            AVAssetTrack *track = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
            // 参数说明:
            // insertTimeRange:源录音文件的的区间
            // ofTrack:插入音频的内容
            // atTime:源音频插入到目标文件开始时间
            // error: 插入失败记录错误
            // 返回:YES表示插入成功,`NO`表示插入失败
            BOOL success = [audioTrack insertTimeRange:timeRange
                                               ofTrack:track
                                                atTime:beginTime
                                                 error:&error];
    #if DEBUG
            // 如果插入失败,打印插入失败信息
            if (!success) {
                NSLog(@"插入音频失败: %@",error);
            }
    #endif
            // 下条记录开始时间
            beginTime = CMTimeAdd(beginTime, CMTimeMake(value, timescale));
        }
    
        // 创建一个导入M4A格式的音频的导出对象
        NSString *presetName = AVAssetExportPresetAppleM4A;
        AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:mixComposition
                                                                               presetName:presetName];
        exportSession.outputURL = outputURL; // 导入音视频的URL
        exportSession.outputFileType = AVFileTypeAppleM4A;  // 导出音视频的文件格式
        exportSession.shouldOptimizeForNetworkUse = YES;
        
        // ⚠️需要在OusiCanteenNotification中的info.plist中设置后台模式 (违规操作,不可上AppStore)
        [exportSession exportAsynchronouslyWithCompletionHandler:^{
            completed(exportSession.error);
            
    #if DEBUG
            if (exportSession.status == AVAssetExportSessionStatusCompleted) {
                NSLog(@"语音文件合并成功");
            }else {
                NSLog(@"语音文件合并失败: %@", exportSession.error);
            }
    #endif
        }];
    }
    
    // MARK: - 辅助方法
    /// 获取保存文件的路径
    - (NSString *)filePath {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *ID = @"group.com.youxin.ousicanteen";
        NSURL *groupURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:ID];
        NSString *groupPath = [groupURL path];
    
        NSString *filePath = [groupPath stringByAppendingPathComponent:@"Library/Sounds"];
        
        if (![fileManager fileExistsAtPath:filePath]) {
            [fileManager createDirectoryAtPath:filePath
                   withIntermediateDirectories:NO
                                    attributes:nil
                                         error:nil];
        }
        
        return filePath;
    }
    
    /// 保留两位小数
    - (NSString *)twoDecimalsWithNum:(NSString *)num {
        return [NSString stringWithFormat:@"%.2Lf", [self roundToFloat:2 num:num]];
    }
    
    /// 四舍五入位 digits 位小数
    - (long double)roundToFloat:(NSUInteger)digits num:(NSString *)num {
        // 使用doubleValue能提高四舍五入的准确性
        double val = num.length < 8 ? num.floatValue : num.doubleValue;
        return roundl(val * powl(10, digits)) / powl(10, digits);
    }
    
    - (NSURL *)urlWithFileName:(NSString *)fileName {
        NSURL *url = nil;
        @try {
            url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:@"mp3"]];
        } @catch (NSException *exception) {
            
        } @finally {
            
        }
        return url;
    }
    
    
    // MARK: - 下载语音文件(未使用)
    /// 需要设置App Transport Security Settings
    - (void)loadWavWithUrl:(NSString *)urlStr{
        NSLog(@"开始下载");
        NSURL *url = [NSURL URLWithString:urlStr];
           //默认的congig
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        
        //session
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
        self.task = [session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (!error) {
                NSLog(@"下载完成");
                NSString *name = [NSString stringWithFormat:@"%u.mp3",arc4random()%50000 ];
                 //获取保存文件的路径
                 NSString *path = self.filePath;
                 //将url对应的文件copy到指定的路径
    
                 NSFileManager *fileManager = [NSFileManager defaultManager];
                 if(![fileManager fileExistsAtPath:path]){
                     [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
                 }
                 NSString * soundStr = [NSString stringWithFormat:@"%@",name];
    
                 NSString *savePath = [path stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",soundStr]];
                 if ([fileManager fileExistsAtPath:savePath]) {
                     [fileManager removeItemAtPath:savePath error:nil];
                    }
                 NSURL *saveURL = [NSURL fileURLWithPath:savePath];
                
                 NSError * saveError;
                 // 文件移动到cache路径中
                 [[NSFileManager defaultManager] moveItemAtURL:location toURL:saveURL error:&saveError];
                 if (!saveError)
                 {
                     self.bestAttemptContent.sound = [UNNotificationSound soundNamed:name];;
                     self.contentHandler(self.bestAttemptContent);
                     
                 }
    
            }else{
                
                NSLog(@"失败");
            }
             
        }];
        
        //启动下载任务
        [_task resume];
    }
    @end
    
    

    相关文章

      网友评论

          本文标题:推送语音播报--简单归纳

          本文链接:https://www.haomeiwen.com/subject/tdutgktx.html