1. 程式人生 > >關於APP上語音播報的完整實現(iOS篇)

關於APP上語音播報的完整實現(iOS篇)

前一段時間,一個支付寶到賬100的鈴聲在網路上火了起來,其實這在APP上,特別支付類的應用裡,經常用到,今天我們談一下其實現方法,給類似這種場景的開發人員一個參考吧。

首先,我們這次是基於推送+語音的方式來實現。

使用sound欄位

我們都知道,我們可以在進行推送的時候,指定sound的檔名,來播放指定聲音檔案。

於是,錄好一個聲音檔案,暫且叫“tts_default.mp3”吧,加入到主工程中。

服務端收到一筆款項的時候,往訊息中心發起一個推送,推送的格式和內容如下:

{"aps":{"alert":"XXX到賬一筆","badge":1,"sound":"tts_default.mp3"}}

這樣,APP

在接受到通知的時候,彈出一個通知框,顯示“XXX到賬一筆”,並伴隨一個聲音,播放的是語音檔案tts_default.mp3。

播報金額

如果收到一筆錢,如果能播放具體金額就更好了,因為金額是變化的,你不可能在工程裡新增許多“tts_default.mp3”檔案,那我們只有合成金額,在AVFoundation裡,有合成聲音的API,在第三方,也有如百度、訊飛一樣的第三方合成聲音的介面,我們測試一下,還是比較生硬。這裡我們仿地鐵、車站的廣播,錄了一些基礎的聲音、和一些數字,我們自己來合成所需的聲音。

假如,你要實現的語音格式是這樣的:錢到啦到賬xx.xx元。

我們錄製並預置了一些語音檔案打在包裡,這些檔案包括:

tts_pre.mp3   對應文字為:錢到啦到賬

tts.yuan.mp3 對應文字為:元。

另外還有一些表表示數字的,如0123456789、十、百、千、萬、點

對應的聲音檔案為:

tts_0.mp3 ~ tts_9.mp3、tts_ten.mp3、tts_hundred.mp3、tts_thousand.mp3、tts_ten_thousand.mp3、tts_dot.mp3
當我們想播放聲音“錢到啦到賬0.25”的時候,我們就可以依次播放聲音檔案:
tts_pre.mp3、tts_0.mp3、tts_dot.mp3、tts_2.mp3、tts_5.mp3、tts.yuan.mp3

就可以了。

這裡牽扯到一個金額轉語音檔案的演算法,後面的Demo有實現,可以參考一下:

-(NSString *)wordsStringFromAmount:(NSString *)numstr;

流程是這樣的:

1、後端收到錢,給商家發起一個推送,格式為:

{"aps":{"alert":"錢到啦到賬0.25元","badge":1,"amount":0.25, "sound":"tts_default.mp3"}} 

2、客戶端收到推送,處理金額欄位amount,轉成對應的播放檔案陣列。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{
    [[BPAudioManager sharedPlayer] playPushInfo:userInfo completed:nil] ;
}

3、開始播放聲音檔案。

後臺播放

當APP在前臺的時候,上面那種處理方法是沒有問題的,在後臺的時候,只會播放一個“tts_default.mp3”這個通用型的語音檔案,也沒有問題的,但是在後臺和APP退出的情況下,playPushInfo這個方法執行一些處理,並播放語音是不可行的,所以還藉助其他的方法,好在蘋果在iOS10的時候,釋出了UNNotificationServiceExtension擴充套件,關於此擴充套件,可以網上選擇一些資料,主要的核心思想就是,在遠端推送到底裝置之前,給你一個修改的機會,我們知道,推送體是有限制的,而且推送體大小也會影響推送的效率,藉助這個,我們可以修改標題、內容,也可以從網路上請求到內容,再去合成一個新的推送。我們這裡不修改內容,主要是用來播放語音。

要使用這個擴充套件,和其他擴充套件一樣,新建一個target,找到這個模版,然後下一步,就好了。

系統會自動實現兩個方法:

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler ;
- (void)serviceExtensionTimeWillExpire;

前者,你需要在這裡做一些操作,修改內容,當你完成後,通知系統,這時候,推送才會顯示出來。我們這裡主要處理推送,並播放聲音;後者會在超時的情況下呼叫。如:

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    //step1: 標記該推送已經在這裡處理過了
    NSMutableDictionary *dict = [self.bestAttemptContent.userInfo mutableCopy] ;
    [dict setObject:[NSNumber numberWithBool:YES] forKey:@"hasHandled"] ;
    self.bestAttemptContent.userInfo = dict ;
    
    //step2: 忽略推送中的預設語音檔案(有可能是那個recieved.mp3)
    self.bestAttemptContent.sound = [UNNotificationSound defaultSound] ;
    
    //step3: 處理推送資訊,播放語音
    [[BPAudioManager sharedPlayer] playPushInfo:self.bestAttemptContent.userInfo completed:^{
        // 播放完成後,通知系統
        self.contentHandler(self.bestAttemptContent);
    }] ;
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    self.contentHandler(self.bestAttemptContent);
}

要啟用UNNotificationServiceExtension擴充套件,需要在欄位中新增mutable-content欄位,所以新的推送體為:

{"aps":{"alert":"錢到啦到賬0.25元","badge":1,"mutable-content":1,"amount":0.25, "sound":"tts_default.mp3"}} 

BPAudioManager

我們定義了一個聲音處理的中間類,因為擴充套件和APP本身都會使用這個類,所以新建這個檔案的時候,注意勾選Targets

- (void) playPushInfo:(NSDictionary *)userInfo completed:(BPAudioPlayCompleted)completed {
    
    //獲取aps
    NSDictionary *aps =  [userInfo objectForKey:@"aps"] ;
    
    //判斷是否需要播報語音,因為所有的推送,都會走到這裡
    BOOL playaudio =  [[aps objectForKey:@"playaudio"] boolValue] ;
    if(!playaudio) {
        if(completed != nil) {
            completed() ;
        }
    }
    // 處理
    else {
        self.completed = completed ;
        NSString *amount = [aps objectForKey:@"amount"] ;
        NSArray* arrAudioFiles = [self getAudioFilesWithAmount:amount] ;
        [self playAudioFiles:arrAudioFiles] ;
    }
}

先處理金額,得到語音檔案的陣列,播放語音這裡直接用迴圈播放的方式了

// 播放聲音檔案
- (void) playAudioFiles {
    // 1.獲取要播放音訊檔案的URL
    NSString *fileName = [audioFiles objectAtIndex:audioIndex] ;
    NSString *path = [NSString stringWithFormat:@"%@/%@",[[NSBundle mainBundle] resourcePath], fileName] ;
    NSURL *fileURL = [NSURL fileURLWithPath:path];
    
    // 2.建立 AVAudioPlayer 物件
    self.audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:fileURL error:nil];
    // 4.設定迴圈播放
    self.audioPlayer.numberOfLoops = 0 ;
    self.audioPlayer.delegate = self;
    // 5.開始播放
    [self.audioPlayer prepareToPlay] ;
    [self.audioPlayer play];
}
// 播放完成回撥
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    audioIndex += 1 ;
    if(audioIndex < audioFiles.count) {
        [self performSelectorOnMainThread:@selector(playAudioFiles) withObject:nil waitUntilDone:NO] ;
    }
    else {
        [self setNormalVolume] ;
        [self disactivePlayback] ;
        [self performSelectorOnMainThread:@selector(playCompleted) withObject:nil waitUntilDone:NO] ;
    }
}

到這裡,基本就完成了,在後臺、退出後臺的情況下,可以正常語音播報了。

音量調節

有時候,我們不小心把聲音關閉了,或者音量很小,或者靜音模式下,那這個時候,播放的聲音就可能聽不見了,為了防止這個情況發生,我們在播放的時候,適當處理一下,是非要有必要的。

// 設定高音量
- (void) setHighVolume {
    MPVolumeView*volumeView = [[MPVolumeViewalloc] init];
    UISlider*volumeViewSlider = nil;
    for(UIView*view in[volumeView subviews]){
        if([view.class.descriptionisEqualToString:@"MPVolumeSlider"]){
            volumeViewSlider = (UISlider*)view;
            break;
        }
    }
    
    // 獲取系統原來的音量,用於還原
    userVolume= volumeViewSlider.value;
    
    // 留點餘地,設定0.9吧, 值在0.0~1.0之間
    if(userVolume< 0.9f) {
        // 改變系統音量
        [volumeViewSlider setValue:0.9fanimated:NO];
        // 發一個事件使之生效
        [volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
    }
}

然後播放完成的時候,會設定會正常音量

// 設定回正常音量
- (void) setNormalVolume {
    MPVolumeView*volumeView = [[MPVolumeViewalloc] init];
    UISlider* volumeViewSlider = nil;
    for(UIView*view in[volumeView subviews]){
        if([view.class.descriptionisEqualToString:@"MPVolumeSlider"]){
            volumeViewSlider = (UISlider*)view;
            break;
        }
    }
    if(volumeViewSlider.value!=userVolume) {
        [volumeViewSlider setValue:userVolumeanimated:NO];
        [volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
    }
}

然後靜音處理:

// 靜音模式下,依然可以播放
- (void) activePlayback {
    [[AVAudioSessionsharedInstance] setCategory:AVAudioSessionCategoryPlaybackerror:NULL];
    [[AVAudioSessionsharedInstance] setActive:YESerror:NULL];
}
//迴歸正常
- (void)disactivePlayback {
    [[AVAudioSessionsharedInstance] setActive:NOerror:NULL];
}

至此,語音播報算是完成了。

1、在iOS10以下,推送利用sound欄位,前臺可以正常播放,後臺、退出的情況下,播放通用聲音。

2、iOS以上,推送增加mutable-content欄位,可以完美播放。

3、我們增加了一些機制,在低音和靜音模式下,也可以正常工作。

附演示Demo

https://github.com/WinterXIE/PushAudio