// // SCFileAudioCaptureSession.m // Snapchat // // Created by Xiaomu Wu on 2/2/17. // Copyright © 2017 Snapchat, Inc. All rights reserved. // #import "SCFileAudioCaptureSession.h" #import #import #import #import @import AudioToolbox; static float const kAudioBufferDurationInSeconds = 0.2; // same as SCAudioCaptureSession static char *const kSCFileAudioCaptureSessionQueueLabel = "com.snapchat.file-audio-capture-session"; @implementation SCFileAudioCaptureSession { SCQueuePerformer *_performer; SCSentinel *_sentinel; NSURL *_fileURL; AudioFileID _audioFile; // audio file AudioStreamBasicDescription _asbd; // audio format (core audio) CMAudioFormatDescriptionRef _formatDescription; // audio format (core media) SInt64 _readCurPacket; // current packet index to read UInt32 _readNumPackets; // number of packets to read every time UInt32 _readNumBytes; // number of bytes to read every time void *_readBuffer; // data buffer to hold read packets } @synthesize delegate = _delegate; #pragma mark - Public - (instancetype)init { self = [super init]; if (self) { _performer = [[SCQueuePerformer alloc] initWithLabel:kSCFileAudioCaptureSessionQueueLabel qualityOfService:QOS_CLASS_UNSPECIFIED queueType:DISPATCH_QUEUE_SERIAL context:SCQueuePerformerContextCamera]; _sentinel = [[SCSentinel alloc] init]; } return self; } - (void)dealloc { if (_audioFile) { AudioFileClose(_audioFile); } if (_formatDescription) { CFRelease(_formatDescription); } if (_readBuffer) { free(_readBuffer); } } - (void)setFileURL:(NSURL *)fileURL { [_performer perform:^{ _fileURL = fileURL; }]; } #pragma mark - SCAudioCaptureSession - (void)beginAudioRecordingAsynchronouslyWithSampleRate:(double)sampleRate // `sampleRate` ignored completionHandler:(audio_capture_session_block)completionHandler { [_performer perform:^{ BOOL succeeded = [self _setup]; int32_t sentinelValue = [_sentinel value]; if (completionHandler) { completionHandler(nil); } if (succeeded) { [_performer perform:^{ SC_GUARD_ELSE_RETURN([_sentinel value] == sentinelValue); [self _read]; } after:kAudioBufferDurationInSeconds]; } }]; } - (void)disposeAudioRecordingSynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler { [_performer performAndWait:^{ [self _teardown]; if (completionHandler) { completionHandler(); } }]; } #pragma mark - Private - (BOOL)_setup { SCAssert([_performer isCurrentPerformer], @""); [_sentinel increment]; OSStatus status = noErr; status = AudioFileOpenURL((__bridge CFURLRef)_fileURL, kAudioFileReadPermission, 0, &_audioFile); if (noErr != status) { SCLogGeneralError(@"Cannot open file at URL %@, error code %d", _fileURL, (int)status); return NO; } _asbd = (AudioStreamBasicDescription){0}; UInt32 asbdSize = sizeof(_asbd); status = AudioFileGetProperty(_audioFile, kAudioFilePropertyDataFormat, &asbdSize, &_asbd); if (noErr != status) { SCLogGeneralError(@"Cannot get audio data format, error code %d", (int)status); AudioFileClose(_audioFile); _audioFile = NULL; return NO; } if (kAudioFormatLinearPCM != _asbd.mFormatID) { SCLogGeneralError(@"Linear PCM is required"); AudioFileClose(_audioFile); _audioFile = NULL; _asbd = (AudioStreamBasicDescription){0}; return NO; } UInt32 aclSize = 0; AudioChannelLayout *acl = NULL; status = AudioFileGetPropertyInfo(_audioFile, kAudioFilePropertyChannelLayout, &aclSize, NULL); if (noErr == status) { acl = malloc(aclSize); status = AudioFileGetProperty(_audioFile, kAudioFilePropertyChannelLayout, &aclSize, acl); if (noErr != status) { aclSize = 0; free(acl); acl = NULL; } } status = CMAudioFormatDescriptionCreate(NULL, &_asbd, aclSize, acl, 0, NULL, NULL, &_formatDescription); if (acl) { free(acl); acl = NULL; } if (noErr != status) { SCLogGeneralError(@"Cannot create format description, error code %d", (int)status); AudioFileClose(_audioFile); _audioFile = NULL; _asbd = (AudioStreamBasicDescription){0}; return NO; } _readCurPacket = 0; _readNumPackets = ceil(_asbd.mSampleRate * kAudioBufferDurationInSeconds); _readNumBytes = _asbd.mBytesPerPacket * _readNumPackets; _readBuffer = malloc(_readNumBytes); return YES; } - (void)_read { SCAssert([_performer isCurrentPerformer], @""); OSStatus status = noErr; UInt32 numBytes = _readNumBytes; UInt32 numPackets = _readNumPackets; status = AudioFileReadPacketData(_audioFile, NO, &numBytes, NULL, _readCurPacket, &numPackets, _readBuffer); if (noErr != status) { SCLogGeneralError(@"Cannot read audio data, error code %d", (int)status); return; } if (0 == numPackets) { return; } CMTime PTS = CMTimeMakeWithSeconds(_readCurPacket / _asbd.mSampleRate, 600); _readCurPacket += numPackets; CMBlockBufferRef dataBuffer = NULL; status = CMBlockBufferCreateWithMemoryBlock(NULL, NULL, numBytes, NULL, NULL, 0, numBytes, 0, &dataBuffer); if (kCMBlockBufferNoErr == status) { if (dataBuffer) { CMBlockBufferReplaceDataBytes(_readBuffer, dataBuffer, 0, numBytes); CMSampleBufferRef sampleBuffer = NULL; CMAudioSampleBufferCreateWithPacketDescriptions(NULL, dataBuffer, true, NULL, NULL, _formatDescription, numPackets, PTS, NULL, &sampleBuffer); if (sampleBuffer) { [_delegate audioCaptureSession:self didOutputSampleBuffer:sampleBuffer]; CFRelease(sampleBuffer); } CFRelease(dataBuffer); } } else { SCLogGeneralError(@"Cannot create data buffer, error code %d", (int)status); } int32_t sentinelValue = [_sentinel value]; [_performer perform:^{ SC_GUARD_ELSE_RETURN([_sentinel value] == sentinelValue); [self _read]; } after:kAudioBufferDurationInSeconds]; } - (void)_teardown { SCAssert([_performer isCurrentPerformer], @""); [_sentinel increment]; if (_audioFile) { AudioFileClose(_audioFile); _audioFile = NULL; } _asbd = (AudioStreamBasicDescription){0}; if (_formatDescription) { CFRelease(_formatDescription); _formatDescription = NULL; } _readCurPacket = 0; _readNumPackets = 0; _readNumBytes = 0; if (_readBuffer) { free(_readBuffer); _readBuffer = NULL; } } @end