My app synthesises audio from a lookup table. It plays audio successfully but crashes the moment I try to stop playing. Audio playback only needs to exit without restarting so the requirements for handling the interruption are basic. I reread Apple’s Audio Session Programming Guide including the section Responding to Interruptions. However the method handleAudioSessionInterruption does not seem to register an interrupt so I’m obviously missing something.
EDIT See my answer. When I began work on this I knew next to nothing about NSNotificationCenter so I welcome any suggestion for improvement.
Two methods set up the audio session to play in the foreground.
- (void)setUpAudio
{
if (_playQueue == NULL)
{
if ([self setUpAudioSession] == TRUE)
{
[self setUpPlayQueue];
[self setUpPlayQueueBuffers];
}
}
}
- (BOOL)setUpAudioSession
{
BOOL success = NO;
NSError *audioSessionError = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
// Set up notifications
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleAudioSessionInterruption:)
name:AVAudioSessionInterruptionNotification
object:session];
// Set category
success = [session setCategory:AVAudioSessionCategoryPlayback
error:&audioSessionError];
if (!success)
{
NSLog(#"%# Error setting category: %#",
NSStringFromSelector(_cmd), [audioSessionError localizedDescription]);
// Exit early
return success;
}
// Set mode
success = [session setMode:AVAudioSessionModeDefault
error:&audioSessionError];
if (!success)
{
NSLog(#"%# Error setting mode: %#",
NSStringFromSelector(_cmd), [audioSessionError localizedDescription]);
// Exit early
return success;
}
// Set some preferred values
NSTimeInterval bufferDuration = .005; // I would prefer a 5ms buffer duration
success = [session setPreferredIOBufferDuration:bufferDuration
error:&audioSessionError];
if (audioSessionError)
{
NSLog(#"Error %ld, %# %i", (long)audioSessionError.code, audioSessionError.localizedDescription, success);
}
double sampleRate = _audioFormat.mSampleRate; // I would prefer a sample rate of 44.1kHz
success = [session setPreferredSampleRate:sampleRate
error:&audioSessionError];
if (audioSessionError)
{
NSLog(#"Error %ld, %# %i", (long)audioSessionError.code, audioSessionError.localizedDescription, success);
}
success = [session setActive:YES
error:&audioSessionError];
if (!success)
{
NSLog(#"%# Error activating %#",
NSStringFromSelector(_cmd), [audioSessionError localizedDescription]);
}
// Get current values
sampleRate = session.sampleRate;
bufferDuration = session.IOBufferDuration;
NSLog(#"Sample Rate:%0.0fHz I/O Buffer Duration:%f", sampleRate, bufferDuration);
return success;
}
And here is the method that handles the interruption when I press the stop button. However it does not respond.
EDIT The correct method needs block, not selector. See my answer.
- (void)handleAudioSessionInterruption:(NSNotification*)notification
{
if (_playQueue)
{
NSNumber *interruptionType = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey];
NSNumber *interruptionOption = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];
NSLog(#"in-app Audio playback will be stopped by %# %lu", notification.name, (unsigned long)interruptionType.unsignedIntegerValue);
switch (interruptionType.unsignedIntegerValue)
{
case AVAudioSessionInterruptionTypeBegan:
{
if (interruptionOption.unsignedIntegerValue == AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)
{
NSLog(#"notify other apps that audio is now available");
}
}
break;
default:
break;
}
}
}
Answer My method to handle AudioSessionInterruption did not subscribe the observer correctly with NSNotificationCentre. This has been fixed by adding observer using block, not selector.
The solution replaces deprecated AVAudioSession delegate methods in AudioBufferPlayer, an extremely fit for purpose audio player initially developed for direct audio synthesis by Matthias Hollejmans. Several deprecated functions including InterruptionListenerCallback were later upgraded by Mario Diana. The solution (below) uses NSNotification allowing users to exit AVAudioSession gracefully by pressing a button.
Here is the relevant code.
PlayViewController.m
UIButton action performs an orderly shutdown of synth, invalidates the timer and posts the notification that will exit AVAudioSession
- (void)fromEscButton:(UIButton*)button
{
[self stopConcertClock];
... // code for Exit PlayViewController not shown
}
- (void)stopConcertClock
{
[_synthLock lock];
[_synth stopAllNotes];
[_synthLock unlock];
[timer invalidate];
timer = nil;
[self postAVAudioSessionInterruptionNotification];
NSLog(#"Esc button pressed or sequence ended. Exit PlayViewController ");
}
- (void) postAVAudioSessionInterruptionNotification
{
[[NSNotificationCenter defaultCenter]
postNotificationName:#"AVAudioSessionInterruptionNotification"
object:self];
}
Initialising the AVAudioSession includes subscribing for a single interruption notification before starting startAudioPlayer in AudioBufferPlayer
- (id)init
{
if (self = [super init])
{
NSLog(#"PlayViewController starts MotionListener and AudioSession");
[self startAudioSession];
}
return self;
}
- (void)startAudioSession
{
// Synth and the AudioBufferPlayer must use the same sample rate.
_synthLock = [[NSLock alloc] init];
float sampleRate = 44100.0f;
// Initialise synth to fill the audio buffer with audio samples.
_synth = [[Synth alloc] initWithSampleRate:sampleRate];
// Initialise the audio buffer.
_player = [[AudioBufferPlayer alloc] initWithSampleRate:sampleRate
channels:1
bitsPerChannel:16
packetsPerBuffer:1024];
_player.gain = 0.9f;
__block __weak PlayViewController *weakSelf = self;
_player.block = ^(AudioQueueBufferRef buffer, AudioStreamBasicDescription audioFormat)
{
PlayViewController *blockSelf = weakSelf;
if (blockSelf != nil)
{
// Lock access to the synth. This callback runs on an internal Audio Queue thread and we don't
// want another thread to change the Synth's state while we're still filling up the audio buffer.
[blockSelf -> _synthLock lock];
// Calculate how many packets fit into this buffer. Remember that a packet equals one frame
// because we are dealing with uncompressed audio; a frame is a set of left+right samples
// for stereo sound, or a single sample for mono sound. Each sample consists of one or more
// bytes. So for 16-bit mono sound, each packet is 2 bytes. For stereo it would be 4 bytes.
int packetsPerBuffer = buffer -> mAudioDataBytesCapacity / audioFormat.mBytesPerPacket;
// Let the Synth write into the buffer. The Synth just knows how to fill up buffers
// in a particular format and does not care where they come from.
int packetsWritten = [blockSelf -> _synth fillBuffer:buffer->mAudioData frames:packetsPerBuffer];
// We have to tell the buffer how many bytes we wrote into it.
buffer -> mAudioDataByteSize = packetsWritten * audioFormat.mBytesPerPacket;
[blockSelf -> _synthLock unlock];
}
};
// Set up notifications
[self subscribeForBlockNotification];
[_player startAudioPlayer];
}
- (void)subscribeForBlockNotification
{
NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:#"AVAudioSessionInterruptionNotification"
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
NSLog(#"Received the notification!");
[_player stopAudioPlayer];
[center removeObserver:token];
}];
}
PlayViewController.h
These are relevant interface settings
#interface PlayViewController : UIViewController <EscButtonDelegate>
{
...
// Initialisation of audio player and synth
AudioBufferPlayer* player;
Synth* synth;
NSLock* synthLock;
}
...
- (AudioBufferPlayer*)player;
- (Synth*)synth;
#end
AudioBufferPlayer.m
- (void)stopAudioPlayer
{
[self stopPlayQueue];
[self tearDownPlayQueue];
[self tearDownAudioSession];
}
- (void)stopPlayQueue
{
if (_audioPlaybackQueue != NULL)
{
AudioQueuePause(_audioPlaybackQueue);
AudioQueueReset(_audioPlaybackQueue);
_playing = NO;
}
}
- (void)tearDownPlayQueue
{
AudioQueueDispose(_audioPlaybackQueue, NO);
_audioPlaybackQueue = NULL;
}
- (BOOL)tearDownAudioSession
{
NSError *deactivationError = nil;
BOOL success = [[AVAudioSession sharedInstance] setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:nil];
if (!success)
{
NSLog(#"%s AVAudioSession Error: %#", __FUNCTION__, deactivationError);
}
return success;
}
Related
Most times I launch the app, the test-to-speech audio and speech recognition work perfectly. But sometimes I launch it and it crashes when first starting speech recognition. It seems to get on a roll and crash several launches in a row, so it is a little bit consistent, once it gets into one of its 'moods' :)
The recognition starts after a TTS introduction and at the same time as TTS says 'listening' - therefore both are active at once. Possibly the audio takes a few milliseconds to change over and gets crashy, but I am not clear how this works or how to prevent it.
I see the following error:
[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false:
[AVAudioIONodeImpl.mm:911:SetOutputFormat: (format.sampleRate == hwFormat.sampleRate)]
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio',
reason: 'required condition is false: format.sampleRate == hwFormat.sampleRate'
I have put some try-catches in, just to see if it prevents this error and it doesn't. I also added a tiny sleep which also made no difference. So I am not even clear which code is causing it. If I put a breakpoint before the removeTapOnBus code, it does not crash until this line executes. If I put the breakpoint on the installTapOnBus line it does not crash until that line. And if I put the breakpoint after the code, it crashes. So it does seem to be this code.
In any case, what am I doing wrong or how could I debug it?
- (void) recordAndRecognizeWithLang:(NSString *) lang
{
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:lang];
self.sfSpeechRecognizer = [[SFSpeechRecognizer alloc] initWithLocale:locale];
if (!self.sfSpeechRecognizer) {
[self sendErrorWithMessage:#"The language is not supported" andCode:7];
} else {
// Cancel the previous task if it's running.
if ( self.recognitionTask ) {
[self.recognitionTask cancel];
self.recognitionTask = nil;
}
//[self initAudioSession];
self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
self.recognitionRequest.shouldReportPartialResults = [[self.command argumentAtIndex:1] boolValue];
// https://developer.apple.com/documentation/speech/sfspeechrecognizerdelegate
// only callback is availabilityDidChange
self.sfSpeechRecognizer.delegate = self;
self.recognitionTask = [self.sfSpeechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult *result, NSError *error) {
NSLog(#"recognise");
if (error) {
NSLog(#"error %ld", error.code);
// code 1 or 203 or 216 = we called abort via self.recognitionTask cancel
// 1101 is thrown when in simulator
// 1700 is when not given permission
if (error.code==203){ //|| error.code==216
// nothing, carry on, this is bullshit, or maybe not...
[self sendErrorWithMessage:#"sfSpeechRecognizer Error" andCode:error.code];
}else{
[self stopAndRelease];
// note: we can't send error back to js as I found it crashes (when recognising, then switch apps, then come back)
[self sendErrorWithMessage:#"sfSpeechRecognizer Error" andCode:error.code];
return;
}
}
if (result) {
NSMutableArray * alternatives = [[NSMutableArray alloc] init];
int maxAlternatives = [[self.command argumentAtIndex:2] intValue];
for ( SFTranscription *transcription in result.transcriptions ) {
if (alternatives.count < maxAlternatives) {
float confMed = 0;
for ( SFTranscriptionSegment *transcriptionSegment in transcription.segments ) {
//NSLog(#"transcriptionSegment.confidence %f", transcriptionSegment.confidence);
if (transcriptionSegment.confidence){
confMed +=transcriptionSegment.confidence;
}
}
NSLog(#"transcriptionSegment.transcript %#", transcription.formattedString);
NSMutableDictionary * resultDict = [[NSMutableDictionary alloc]init];
[resultDict setValue:transcription.formattedString forKey:#"transcript"];
[resultDict setValue:[NSNumber numberWithBool:result.isFinal] forKey:#"final"];
float conf = 0;
if (confMed && transcription.segments && transcription.segments.count && transcription.segments.count>0){
conf = confMed/transcription.segments.count;
}
[resultDict setValue:[NSNumber numberWithFloat:conf]forKey:#"confidence"];
[alternatives addObject:resultDict];
}
}
[self sendResults:#[alternatives]];
if ( result.isFinal ) {
//NSLog(#"recog: isFinal");
[self stopAndRelease];
}
}
}];
//[self.audioEngine.inputNode disconnectNodeInput:0];
AVAudioFormat *recordingFormat = [self.audioEngine.inputNode outputFormatForBus:0];
//AVAudioFormat *recordingFormat = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:1];
NSLog(#"samplerate=%f", recordingFormat.sampleRate);
NSLog(#"channelCount=%i", recordingFormat.channelCount);
// tried this but does not prevent crashing
//if (recordingFormat.sampleRate <= 0) {
// [self.audioEngine.inputNode reset];
// recordingFormat = [[self.audioEngine inputNode] outputFormatForBus:0];
//}
sleep(1); // to prevent random crashes
#try {
[self.audioEngine.inputNode removeTapOnBus:0];
} #catch (NSException *exception) {
NSLog(#"removeTapOnBus exception");
}
sleep(1); // to prevent random crashes
#try {
NSLog(#"install tap on bus");
[self.audioEngine.inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
//NSLog(#"tap");
[self.recognitionRequest appendAudioPCMBuffer:buffer];
}];
} #catch (NSException *exception) {
NSLog(#"installTapOnBus exception");
}
sleep(1); // to prevent random crashes
[self.audioEngine prepare];
NSError* error = nil;
BOOL isOK = [self.audioEngine startAndReturnError:&error];
if (!isOK){
NSLog(#"audioEngine startAndReturnError returned false");
}
if (error){
NSLog(#"audioEngine startAndReturnError error");
}
}
I am using the iOS speech recognition API from an Objective-C iOS app.
It works on iPhone 6, 7, but does not work on iPhone 5 (iOS, 10.2.1).
Also note it works on iPhone 5s, just not iPhone 5.
Is the iOS speech API suppose to work on iPhone 5? Do you have to do anything different to get it to work, or does anyone know what the issue could be?
The basic code is below. No errors occur, and the mic volume is detected, but no speech is detected.
if (audioEngine != NULL) {
[audioEngine stop];
[speechTask cancel];
AVAudioInputNode* inputNode = [audioEngine inputNode];
[inputNode removeTapOnBus: 0];
}
recording = YES;
micButton.selected = YES;
//NSLog(#"Starting recording... SFSpeechRecognizer Available? %d", [speechRecognizer isAvailable]);
NSError * outError;
//NSLog(#"AUDIO SESSION CATEGORY0: %#", [[AVAudioSession sharedInstance] category]);
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory: AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:&outError];
[audioSession setMode: AVAudioSessionModeMeasurement error:&outError];
[audioSession setActive: true withOptions: AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&outError];
SFSpeechAudioBufferRecognitionRequest* speechRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];
//NSLog(#"AUDIO SESSION CATEGORY1: %#", [[AVAudioSession sharedInstance] category]);
if (speechRequest == nil) {
NSLog(#"Unable to create SFSpeechAudioBufferRecognitionRequest.");
return;
}
speechDetectionSamples = 0;
// This some how fixes a crash on iPhone 7
// Seems like a bug in iOS ARC/lack of gc
AVAudioEngine* temp = audioEngine;
audioEngine = [[AVAudioEngine alloc] init];
AVAudioInputNode* inputNode = [audioEngine inputNode];
speechRequest.shouldReportPartialResults = true;
// iOS speech does not detect end of speech, so must track silence.
lastSpeechDetected = -1;
speechTask = [speechRecognizer recognitionTaskWithRequest: speechRequest delegate: self];
[inputNode installTapOnBus:0 bufferSize: 4096 format: [inputNode outputFormatForBus:0] block:^(AVAudioPCMBuffer* buffer, AVAudioTime* when) {
#try {
long millis = [[NSDate date] timeIntervalSince1970] * 1000;
if (lastSpeechDetected != -1 && ((millis - lastSpeechDetected) > 1000)) {
lastSpeechDetected = -1;
[speechTask finish];
return;
}
[speechRequest appendAudioPCMBuffer: buffer];
//Calculate volume level
if ([buffer floatChannelData] != nil) {
float volume = fabsf(*buffer.floatChannelData[0]);
if (volume >= speechDetectionThreshold) {
speechDetectionSamples++;
if (speechDetectionSamples >= speechDetectionSamplesNeeded) {
//Need to change mic button image in main thread
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
[micButton setImage: [UIImage imageNamed: #"micRecording"] forState: UIControlStateSelected];
}];
}
} else {
speechDetectionSamples = 0;
}
}
}
#catch (NSException * e) {
NSLog(#"Exception: %#", e);
}
}];
[audioEngine prepare];
[audioEngine startAndReturnError: &outError];
NSLog(#"Error %#", outError);
I think the bug is here in this code:
long millis = [[NSDate date] timeIntervalSince1970] * 1000;
The 32-bit devices(iPhone 5 is a 32-bit device), can save maximum number upto 2^32-1 i.e 2,147,483,647.
I checked on iPhone 5 simulator, the millis is having a negative value. In the code snippet you have posted, there is no mention of how lastSpeechDetected is getting set after initially setting it to -1, but if somehow ((millis - lastSpeechDetected) > 1000) is true, then it will enter the if-block and finish the speech task.
I have an application with multiple AudioSession's and their own aduioRouteChangeCallback's. I need a way of seeing what the current or previous Audio Category was in order to display the proper message to the user.
Does anyone know a way this can be done? From what I've seen on the web so far my hopes are not high that this is possible from what I've found on the web. Any help is greatly appreciate because I'm working against a deadline.
Below is an example of one of my audioRouteChangeCallback,
- (void) audioRouteChangeListener: (NSNotification*)notification {
// Initiallize dictionary with notification and grab route change reason
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDictvalueForKey:
AVAudioSessionRouteChangeReasonKey] integerValue];
NSLog(#"MADE IT: sensorAudioRouteChageListener");
switch (routeChangeReason) {
// Sensor inserted
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
// Start IO communication
[self startCollecting];
NSLog(#"Sensor INSERTED");
break;
// Sensor removed
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
// Stop IO audio unit
[self stopCollecting];
NSLog(#"Sensor REMOVED");
break;
// Category changed from PlayAndRecord
case AVAudioSessionRouteChangeReasonCategoryChange:
// Stop IO audio unit
[self stopCollecting];
NSLog(#"Category CHANGED");
break;
default:
NSLog(#"Blowing it in- audioRouteChangeListener with route change reason: %ld",
(long)routeChangeReason);
break;
}
}
The session is initialized in the objects init function,
- (id) init {
self = [super init];
if (!self) return nil;
// Set up AVAudioSession
self->noiseAudioSession = [AVAudioSession sharedInstance];
BOOL success;
NSError *error;
success = [self->noiseAudioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
if (!success) NSLog(#"ERROR initNoiseRecorder: AVAudioSession failed overrideOutputAudio- %#", error);
success = [self->noiseAudioSession setActive:YES error:&error];
if (!success) NSLog(#"ERROR initNoiseRecorder: AVAudioSession failed activating- %#", error);
return self;
}
My iOS 7 app vocalizes texts when necessary.
What I'd like to do is enable the user to listen to his music or podcasts (or any other app using audio) while mine is running.
The expected behavior is that others audio either mixe or duck when my app speaks, then the others audio resume his volume at the initial level right after.
I have tried many ways to achieve this goal, but nothing is good enough, as I list the issues I face, after the code.
My current implementation is based on creating a session prior to play or text-to-speech as follows:
+ (void)setAudioActive {
[[self class] setSessionActiveWithMixing:YES];
}
After the play/speech, I set i to idled as follows:
+ (void)setAudioIdle {
[[self class] setSessionActiveWithMixing:NO];
}
The core function which handle the session setup accordingly to the active parameter, as follows:
+ (void)setSessionActiveWithMixing:(BOOL)active
{
NSError *error = NULL;
BOOL success;
AVAudioSession *session = [AVAudioSession sharedInstance];
static NSInteger counter = 0;
success = [session setActive:NO error:&error];
if (error) {
DDLogError(#"startAudioMixAndBackground: session setActive:NO, %#", error.description);
}
else {
counter--; if (counter<0) counter = 0;
}
if (active) {
AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionAllowBluetooth
//|AVAudioSessionCategoryOptionDefaultToSpeaker
|AVAudioSessionCategoryOptionDuckOthers
;
success = [session setCategory://AVAudioSessionCategoryPlayback
AVAudioSessionCategoryPlayAndRecord
withOptions: options
error: &error];
if (error) {
// Do some error handling
DDLogError(#"startAudioMixAndBackground: setCategory:AVAudioSessionCategoryPlayback, %#", error.description);
}
else {
//activate the audio session
success = [session setActive:YES error:&error];
if (error) {
DDLogError(#"startAudioMixAndBackground: session setActive:YES, %#", error.description);
}
else {
counter++;
}
}
}
DDLogInfo(#"Audio session counter is: %ld",counter);
}
My current issues are:
1) When my app start to speak, I hear some king of glitch in the sound, which makes it not nice;
2) When I connect the route to bluetooth, the underlying audio (say the Podcast or ipod music) goes very low and sounds noisy, which make my solution merely unusable, my users will reject this level au poor quality.
3) When others bluetooth connected devices tried to emit sounds (say a GPS in a car or instance), my App does not receive any interrupts (or I handle it wrongly), see my code as follows:
- (void)startAudioMixAndBackground {
// initialize our AudioSession -
// this function has to be called once before calling any other AudioSession functions
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(audioSessionDidChangeInterruptionType:)
name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
// set our default audio session state
[[self class] setAudioIdle];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
if ([self canBecomeFirstResponder]) {
[self becomeFirstResponder];
}
#synchronized(self) {
self.okToPlaySound = YES;
}
//MPVolumeSettingsAlertShow();
}
// want remote control events (via Control Center, headphones, bluetooth, AirPlay, etc.)
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
if (event.type == UIEventTypeRemoteControl)
{
switch(event.subtype)
{
case UIEventSubtypeRemoteControlPause:
case UIEventSubtypeRemoteControlStop:
[[self class] setAudioIdle];
break;
case UIEventSubtypeRemoteControlPlay:
[[self class] setAudioActive];
break;
default:
break;
}
}
}
#pragma mark - Audio Support
- (void)audioSessionDidChangeInterruptionType:(NSNotification *)notification
{
AVAudioSessionInterruptionType interruptionType = [[[notification userInfo]
objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
if (AVAudioSessionInterruptionTypeBegan == interruptionType)
{
DDLogVerbose(#"Session interrupted: --- Begin Interruption ---");
}
else if (AVAudioSessionInterruptionTypeEnded == interruptionType)
{
DDLogVerbose(#"Session interrupted: --- End Interruption ---");
}
}
Your issue is most likely due to the category you are setting: AVAudioSessionCategoryPlayAndRecord. The PlayAndRecord category does not allow your app to mix/duck audio with other apps. You should reference the docs on Audio Session Categories again here: https://developer.apple.com/library/ios/documentation/avfoundation/reference/AVAudioSession_ClassReference/Reference/Reference.html. It seems like AVAudioSessionCategoryAmbient is probably more what your looking for.
i had to convert big file size song from iTunes library to a smaller 8K song file.
As i did the converting async, the bool always return true even though writing to doc folder are not completed. At the moment i'm using a delay of 10sec before i called the function again and it works fine on the interim for iPhone 5s, but i would like to cater on the slower devices.
kindly give me some pointer / recommendation on my code.
-(void)startUploadSongAnalysis
{
[self updateProgressYForID3NForUpload:NO];
if ([self.uploadWorkingAray count]>=1)
{
Song *songVar = [self.uploadWorkingAray objectAtIndex:0];//core data var
NSLog(#"songVar %#",songVar.songName);
NSLog(#"songVar %#",songVar.songURL);
NSURL *songU = [NSURL URLWithString:songVar.songURL]; //URL of iTunes Lib
// self.asset = [AVAsset assetWithURL:songU];
// NSLog(#"asset %#",self.asset);
NSError *error;
NSString *subString = [[songVar.songURL componentsSeparatedByString:#"id="] lastObject];
NSString *savedPath = [self.documentsDir stringByAppendingPathComponent:[NSString stringWithFormat:#"audio%#.m4a",subString]];//save file name of converted 8kb song
NSString *subStringPath = [NSString stringWithFormat:#"audio%#.m4a",subString];
if ([self.fileManager fileExistsAtPath:savedPath] == YES)
[self.fileManager removeItemAtPath:savedPath error:&error];
NSLog(#"cacheDir %#",savedPath);
//export low bitrate song to cache
if ([self exportAudio:[AVAsset assetWithURL:songU] toFilePath:savedPath]) // HERE IS THE PROBLEM, this return true even the writing is not completed cos when i upload to my web server, it will say song file corrupted
{
// [self performSelector:#selector(sendSongForUpload:) withObject:subStringPath afterDelay:1];
[self sendRequest:2 andPath:subStringPath andSongDBItem:songVar];
}
else
{
NSLog(#"song too short, skipped");
[self.uploadWorkingAray removeObjectAtIndex:0];
[self.songNotFoundArray addObject:songVar];
[self startUploadSongAnalysis];
}
}
else //uploadWorkingAray empty
{
NSLog(#"save changes");
[[VPPCoreData sharedInstance] saveAllChanges];
}
}
#pragma mark song exporter to doc folder
- (BOOL)exportAudio:(AVAsset *)avAsset toFilePath:(NSString *)filePath
{
CMTime assetTime = [avAsset duration];
Float64 duration = CMTimeGetSeconds(assetTime);
if (duration < 40.0) return NO; // if song too short return no
// get the first audio track
NSArray *tracks = [avAsset tracksWithMediaType:AVMediaTypeAudio];
if ([tracks count] == 0) return NO;
NSError *readerError = nil;
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:avAsset error:&readerError];
//AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:avAsset error:&readerError]; // both works the same ?
AVAssetReaderOutput *readerOutput = [AVAssetReaderAudioMixOutput
assetReaderAudioMixOutputWithAudioTracks:avAsset.tracks
audioSettings: nil];
if (! [reader canAddOutput: readerOutput])
{
NSLog (#"can't add reader output...!");
return NO;
}
else
{
[reader addOutput:readerOutput];
}
// writer AVFileTypeCoreAudioFormat AVFileTypeAppleM4A
NSError *writerError = nil;
AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:filePath]
fileType:AVFileTypeAppleM4A
error:&writerError];
//NSLog(#"writer %#",writer);
AudioChannelLayout channelLayout;
memset(&channelLayout, 0, sizeof(AudioChannelLayout));
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
// use different values to affect the downsampling/compression
// NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
// [NSNumber numberWithInt: kAudioFormatMPEG4AAC], AVFormatIDKey,
// [NSNumber numberWithFloat:16000.0], AVSampleRateKey,
// [NSNumber numberWithInt:2], AVNumberOfChannelsKey,
// [NSNumber numberWithInt:128000], AVEncoderBitRateKey,
// [NSData dataWithBytes:&channelLayout length:sizeof(AudioChannelLayout)], AVChannelLayoutKey,
// nil];
NSDictionary *outputSettings = #{AVFormatIDKey: #(kAudioFormatMPEG4AAC),
AVEncoderBitRateKey: #(8000),
AVNumberOfChannelsKey: #(1),
AVSampleRateKey: #(8000)};
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:outputSettings];
//\Add inputs to Write
NSParameterAssert(writerInput);
NSAssert([writer canAddInput:writerInput], #"Cannot write to this type of audio input" );
if ([writer canAddInput:writerInput])
{
[writer addInput:writerInput];
}
else
{
NSLog (#"can't add asset writer input... die!");
return NO;
}
[writerInput setExpectsMediaDataInRealTime:NO];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
[reader startReading];
__block UInt64 convertedByteCount = 0;
__block BOOL returnValue;
__block CMSampleBufferRef nextBuffer;
dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
[writerInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^{
// NSLog(#"Asset Writer ready : %d", writerInput.readyForMoreMediaData);
while (writerInput.readyForMoreMediaData)
{
nextBuffer = [readerOutput copyNextSampleBuffer];
if (nextBuffer)
{
[writerInput appendSampleBuffer: nextBuffer];
convertedByteCount += CMSampleBufferGetTotalSampleSize (nextBuffer);
//NSNumber *convertedByteCountNumber = [NSNumber numberWithLong:convertedByteCount];
//NSLog (#"writing");
CFRelease(nextBuffer);
}
else
{
[writerInput markAsFinished];
[writer finishWritingWithCompletionHandler:^{
if (AVAssetWriterStatusCompleted == writer.status)
{
NSLog(#"Writer completed");
returnValue = YES; //I NEED TO RETURN SOMETHING FROM HERE AFTER WRITING COMPLETED
dispatch_async(mediaInputQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
// add this to the main queue as the last item in my serial queue
// when I get to this point I know everything in my queue has been run
NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
attributesOfItemAtPath:filePath
error:nil];
NSLog (#"done. file size is %lld",
[outputFileAttributes fileSize]);
});
});
}
else if (AVAssetWriterStatusFailed == writer.status)
{
[writer cancelWriting];
[reader cancelReading];
NSLog(#"Writer failed");
return;
}
else
{
NSLog(#"Export Session Status: %d", writer.status);
}
}];
break;
}
}
}];
tracks = nil;
writer = nil;
writerInput = nil;
reader = nil;
readerOutput=nil;
mediaInputQueue = nil;
return returnValue;
//return YES;
}
Your method exportAudio:toFilePath: is actually an asynchronous method and requires a few fixes to become a proper asynchronous method.
First, you should provide a completion handler in order to signal the call-site that the underlying task has been finished:
- (void)exportAudio:(AVAsset *)avAsset
toFilePath:(NSString *)filePath
completion:(completion_t)completionHandler;
Note, that the result of the method is passed through the completion handler, whose signature might be as follows:
typedef void (^completion_t)(id result);
where parameter result is the eventual result of the method. You should always return an NSError object when anything goes wrong when setting up the various objects within the method - even though, the method could return an immediate result indicating an error.
Next, if you take a look into to documentation you can read:
requestMediaDataWhenReadyOnQueue:usingBlock:
- (void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queue
usingBlock:(void (^)(void))block
Discussion
The block should append media data to the input either until the input’s readyForMoreMediaData property becomes NO or until there is no more media data to supply (at which point it may choose to mark the input as finished using markAsFinished). The block should then exit. After the block exits, if the input has not been marked as finished, once the input has processed the media data it has received and becomes ready for more media data again, it will invoke the block again in order to obtain more.
You should now be quite sure when your task is actually finished. You determine this within the block which is passed to the method requestMediaDataWhenReadyOnQueue:usingBlock:.
When the task is finished you call the completion handler completionHandler provided in
method exportAudio:toFilePath:completion:.
Of course, you need to fix your implementation, e.g. having the method ending with
tracks = nil;
writer = nil;
writerInput = nil;
reader = nil;
readerOutput=nil;
mediaInputQueue = nil;
return returnValue;
//return YES;
}
makes certainly no sense. Cleaning up and returning a result shall be done when the asynchronous task is actually finished. Unless an error occurs during setup, you need to determine this in the block passed to the method requestMediaDataWhenReadyOnQueue:usingBlock:.
In any case, in order to signal the result to the call-site call the completion handler completionHandler and pass a result object, e.g. if it succeeded the URL where it has been saved, otherwise an NSError object.
Now, since our method startUploadSongAnalysis is calling an asynchronous method, this method inevitable becomes asynchronous as well!
If I understood your original code correctly, you are invoking it recursively in order to process a number of assets. In order to implement this correctly, you need a few fixes shown below. The resulting "construct" is NOT a recursive method though, but instead an iteratively invocation of an asynchronous method ("asynchronous loop").
You may or may not provide a completion handler - same as above. It's up to you - but I would recommend it, it won't hurt to know when all assets have been processed. It may look as follows:
-(void)startUploadSongAnalysisWithCompletion:(completion_t)completionHandler
{
[self updateProgressYForID3NForUpload:NO];
// *** check for break condition: ***
if ([self.uploadWorkingAray count]>=1)
{
... stuff
//export low bitrate song to cache
[self exportAudio:[AVAsset assetWithURL:songU]
toFilePath:savedPath
completion:^(id urlOrError)
{
if ([urlOrError isKindOfClass[NSError class]]) {
// Error occurred:
NSLog(#"Error: %#", urlOrError);
// There are two alternatives to proceed:
// A) Ignore or remember the error and proceed with the next asset.
// In this case, it would be best to have a result array
// containing all the results. Then, invoke
// startUploadSongAnalysisWithCompletion: in order to proceed
// with the next asset.
//
// B) Stop with error.
// Don't call startUploadSongAnalysisWithCompletion: but
// instead invoke the completion handler passing it the error.
// A:
// possibly dispatch to a sync queue or the main thread!
[self.uploadWorkingAray removeObjectAtIndex:0];
[self.songNotFoundArray addObject:songVar];
// *** next song: ***
[self startUploadSongAnalysisWithCompletion:completionHandler];
}
else {
// Success:
// *** next song: ***
NSURL* url = urlOrError;
[self startUploadSongAnalysisWithCompletion:completionHandler];
}
}];
}
else //uploadWorkingAray empty
{
NSLog(#"save changes");
[[VPPCoreData sharedInstance] saveAllChanges];
// *** signal completion ***
if (completionHandler) {
completionHandler(#"OK");
}
}
}
I am not sure, but can not you send a call to a method like following
dispatch_async(mediaInputQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
// add this to the main queue as the last item in my serial queue
// when I get to this point I know everything in my queue has been run
NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
attributesOfItemAtPath:filePath
error:nil];
NSLog (#"done. file size is %lld",
[outputFileAttributes fileSize]);
//calling the following method after completing the queue
[self printMe];
});
});
-(void)printMe{
NSLog(#"queue complete...");
//Do the next job, may be the following task !!!
if ([self exportAudio:[AVAsset assetWithURL:songU] toFilePath:savedPath]) // HERE IS THE PROBLEM, this return true even the writing is not completed cos when i upload to my web server, it will say song file corrupted
{
// [self performSelector:#selector(sendSongForUpload:) withObject:subStringPath afterDelay:1];
[self sendRequest:2 andPath:subStringPath andSongDBItem:songVar];
}
else
{
NSLog(#"song too short, skipped");
[self.uploadWorkingAray removeObjectAtIndex:0];
[self.songNotFoundArray addObject:songVar];
[self startUploadSongAnalysis];
}
}