diff --git a/SmartDeviceLink/SDLAudioFile.h b/SmartDeviceLink/SDLAudioFile.h index e34c37a8d..794c50cab 100755 --- a/SmartDeviceLink/SDLAudioFile.h +++ b/SmartDeviceLink/SDLAudioFile.h @@ -12,21 +12,49 @@ NS_ASSUME_NONNULL_BEGIN @interface SDLAudioFile : NSObject -@property (copy, nonatomic, readonly) NSURL *inputFileURL; +/** + If initialized with a file URL, the file URL it came from + */ +@property (nullable, copy, nonatomic, readonly) NSURL *inputFileURL; -@property (copy, nonatomic, readonly) NSURL *outputFileURL; +/** + If initialized with a file URL, where the transcoder should produce the transcoded PCM audio file + */ +@property (nullable, copy, nonatomic, readonly) NSURL *outputFileURL; /** In seconds. UINT32_MAX if unknown. */ @property (assign, nonatomic) UInt32 estimatedDuration; +/** + The PCM audio data to be transferred and played + */ @property (copy, nonatomic, readonly) NSData *data; +/** + The size of the PCM audio data in bytes + */ @property (assign, nonatomic, readonly) unsigned long long fileSize; +/** + Initialize an audio file to be queued and played + + @param inputURL The file that exists on the device to be transcoded and queued + @param outputURL The target URL that the transcoded file will be output to + @param duration The duration of the file + @return The audio file object + */ - (instancetype)initWithInputFileURL:(NSURL *)inputURL outputFileURL:(NSURL *)outputURL estimatedDuration:(UInt32)duration; +/** + Initialize a buffer of PCM audio data to be queued and played + + @param data The PCM audio data buffer + @return The audio file object + */ +- (instancetype)initWithData:(NSData *)data; + @end NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/SDLAudioFile.m b/SmartDeviceLink/SDLAudioFile.m index 1d0e2ef98..46b50f540 100755 --- a/SmartDeviceLink/SDLAudioFile.m +++ b/SmartDeviceLink/SDLAudioFile.m @@ -32,6 +32,15 @@ - (instancetype)initWithInputFileURL:(NSURL *)inputURL outputFileURL:(NSURL *)ou return self; } +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (!self) { return nil; } + + _data = data; + + return self; +} + - (NSData *)data { if (_data.length == 0) { return [NSData dataWithContentsOfURL:_outputFileURL]; diff --git a/SmartDeviceLink/SDLAudioStreamManager.h b/SmartDeviceLink/SDLAudioStreamManager.h index e6608f8e1..b405cae5d 100755 --- a/SmartDeviceLink/SDLAudioStreamManager.h +++ b/SmartDeviceLink/SDLAudioStreamManager.h @@ -66,6 +66,22 @@ typedef NS_ENUM(NSInteger, SDLAudioStreamManagerError) { */ - (void)pushWithFileURL:(NSURL *)fileURL; +/** + Push a new audio buffer onto the queue. Call `playNextWhenReady` to start playing the pushed audio buffer. + + This data must be of the required PCM format. See SDLSystemCapabilityManager.pcmStreamCapability and SDLAudioPassThruCapability.h. + + This is *an example* of a PCM format used by some head units: + - audioType: PCM + - samplingRate: 16kHZ + - bitsPerSample: 16 bits + + There is generally only one channel to the data. + + @param data The audio buffer to be pushed onto the queue + */ +- (void)pushWithData:(NSData *)data; + /** Play the next item in the queue. If an item is currently playing, it will continue playing and this item will begin playing after it is completed. diff --git a/SmartDeviceLink/SDLAudioStreamManager.m b/SmartDeviceLink/SDLAudioStreamManager.m index 69312b0b5..ff16f2b16 100755 --- a/SmartDeviceLink/SDLAudioStreamManager.m +++ b/SmartDeviceLink/SDLAudioStreamManager.m @@ -46,10 +46,22 @@ - (instancetype)initWithManager:(id)streamManager return self; } +- (void)stop { + dispatch_async(_audioQueue, ^{ + self.shouldPlayWhenReady = NO; + [self.mutableQueue removeAllObjects]; + }); +} + +#pragma mark - Getters + - (NSArray *)queue { return [_mutableQueue copy]; } +#pragma mark - Pushing to the Queue +#pragma mark Files + - (void)pushWithFileURL:(NSURL *)fileURL { dispatch_async(_audioQueue, ^{ [self sdl_pushWithContentsOfURL:fileURL]; @@ -79,6 +91,21 @@ - (void)sdl_pushWithContentsOfURL:(NSURL *)fileURL { } } +#pragma mark Raw Data + +- (void)pushWithData:(NSData *)data { + dispatch_async(_audioQueue, ^{ + [self sdl_pushWithData:data]; + }); +} + +- (void)sdl_pushWithData:(NSData *)data { + SDLAudioFile *audioFile = [[SDLAudioFile alloc] initWithData:data]; + [self.mutableQueue addObject:audioFile]; +} + +#pragma mark Playing from the Queue + - (void)playNextWhenReady { dispatch_async(_audioQueue, ^{ [self sdl_playNextWhenReady]; @@ -104,34 +131,46 @@ - (void)sdl_playNextWhenReady { [self.mutableQueue removeObjectAtIndex:0]; // Strip the first bunch of bytes (because of how Apple outputs the data) and send to the audio stream, if we don't do this, it will make a weird click sound + NSData *audioData = nil; + if (file.inputFileURL != nil) { + audioData = [file.data subdataWithRange:NSMakeRange(5760, (file.data.length - 5760))]; + } else { + audioData = file.data; + } + + // Send the audio file, which starts it playing immediately SDLLogD(@"Playing audio file: %@", file); - NSData *audioData = [file.data subdataWithRange:NSMakeRange(5760, (file.data.length - 5760))]; __block BOOL success = [self.streamManager sendAudioData:audioData]; self.playing = YES; + // Determine the length of the audio PCM data and perform a few items once the audio has finished playing float audioLengthSecs = (float)audioData.length / (float)32000.0; __weak typeof(self) weakself = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(audioLengthSecs * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - weakself.playing = NO; + __strong typeof(weakself) strongSelf = weakself; + + strongSelf.playing = NO; NSError *error = nil; - if (weakself.delegate != nil) { - [weakself.delegate audioStreamManager:weakself fileDidFinishPlaying:file.inputFileURL successfully:success]; + if (strongSelf.delegate != nil) { + if (file.inputFileURL != nil) { + [strongSelf.delegate audioStreamManager:strongSelf fileDidFinishPlaying:file.inputFileURL successfully:success]; + } else if ([strongSelf.delegate respondsToSelector:@selector(audioStreamManager:dataBufferDidFinishPlayingSuccessfully:)]) { + [strongSelf.delegate audioStreamManager:strongSelf dataBufferDidFinishPlayingSuccessfully:success]; + } } + SDLLogD(@"Ending Audio file: %@", file); [[NSFileManager defaultManager] removeItemAtURL:file.outputFileURL error:&error]; - if (weakself.delegate != nil && error != nil) { - [weakself.delegate audioStreamManager:weakself errorDidOccurForFile:file.inputFileURL error:error]; + if (strongSelf.delegate != nil && error != nil) { + if (file.inputFileURL != nil) { + [strongSelf.delegate audioStreamManager:strongSelf errorDidOccurForFile:file.inputFileURL error:error]; + } else if ([strongSelf.delegate respondsToSelector:@selector(audioStreamManager:errorDidOccurForDataBuffer:)]) { + [strongSelf.delegate audioStreamManager:strongSelf errorDidOccurForDataBuffer:error]; + } } }); } -- (void)stop { - dispatch_async(_audioQueue, ^{ - self.shouldPlayWhenReady = NO; - [self.mutableQueue removeAllObjects]; - }); -} - @end NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/SDLAudioStreamManagerDelegate.h b/SmartDeviceLink/SDLAudioStreamManagerDelegate.h index acad1472c..04235d95d 100755 --- a/SmartDeviceLink/SDLAudioStreamManagerDelegate.h +++ b/SmartDeviceLink/SDLAudioStreamManagerDelegate.h @@ -29,12 +29,30 @@ NS_ASSUME_NONNULL_BEGIN /** Called when a file from the SDLAudioStreamManager could not play - @param audioManager A reference to the audio stream manager + @param audioManager A reference to the audio stream manager @param fileURL The URL that failed @param error The error that occurred */ - (void)audioStreamManager:(SDLAudioStreamManager *)audioManager errorDidOccurForFile:(NSURL *)fileURL error:(NSError *)error; +@optional + +/** + Called when a data buffer from the SDLAudioStreamManager finishes playing + + @param audioManager A reference to the audio stream manager + @param successfully Whether or not the data buffer played successfully + */ +- (void)audioStreamManager:(SDLAudioStreamManager *)audioManager dataBufferDidFinishPlayingSuccessfully:(BOOL)successfully; + +/** + Called when a data buffer from the SDLAudioStreamManager could not play + + @param audioManager A reference to the audio stream manager + @param error The error that occurred + */ +- (void)audioStreamManager:(SDLAudioStreamManager *)audioManager errorDidOccurForDataBuffer:(NSError *)error; + @end NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLink/SDLStreamingMediaManager.h b/SmartDeviceLink/SDLStreamingMediaManager.h index c306f8550..8e96e7ebb 100644 --- a/SmartDeviceLink/SDLStreamingMediaManager.h +++ b/SmartDeviceLink/SDLStreamingMediaManager.h @@ -170,7 +170,9 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)sendVideoData:(CVImageBufferRef)imageBuffer presentationTimestamp:(CMTime)presentationTimestamp; /** - * This method receives PCM audio data and will attempt to send that data across to the head unit for immediate playback + * This method receives PCM audio data and will attempt to send that data across to the head unit for immediate playback. + * + * NOTE: See the `.audioManager` (SDLAudioStreamManager) `pushWithData:` method for a more modern API. * * @param audioData The data in PCM audio format, to be played * diff --git a/SmartDeviceLinkTests/DevAPISpecs/SDLAudioStreamManagerSpec.m b/SmartDeviceLinkTests/DevAPISpecs/SDLAudioStreamManagerSpec.m index a087ecb5a..e538122f5 100644 --- a/SmartDeviceLinkTests/DevAPISpecs/SDLAudioStreamManagerSpec.m +++ b/SmartDeviceLinkTests/DevAPISpecs/SDLAudioStreamManagerSpec.m @@ -10,6 +10,7 @@ __block SDLAudioStreamManager *testManager = nil; __block SDLStreamingAudioManagerMock *mockAudioManager = nil; __block NSURL *testAudioFileURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"testAudio" withExtension:@"mp3"]; + __block NSData *testAudioFileData = [NSData dataWithContentsOfURL:testAudioFileURL options:0 error:nil]; beforeEach(^{ mockAudioManager = [[SDLStreamingAudioManagerMock alloc] init]; @@ -27,30 +28,93 @@ }); describe(@"when audio streaming is not connected", ^{ + context(@"with a file URL", ^{ + beforeEach(^{ + mockAudioManager.audioConnected = NO; + [testManager pushWithFileURL:testAudioFileURL]; + + [NSThread sleepForTimeInterval:0.5]; + }); + + describe(@"after attempting to play the file", ^{ + beforeEach(^{ + [mockAudioManager clearData]; + [testManager playNextWhenReady]; + }); + + it(@"should fail to send data", ^{ + expect(mockAudioManager.dataSinceClear.length).to(equal(0)); + expect(mockAudioManager.error.code).toEventually(equal(SDLAudioStreamManagerErrorNotConnected)); + }); + }); + }); + + context(@"with a data buffer", ^{ + beforeEach(^{ + mockAudioManager.audioConnected = NO; + [testManager pushWithData:testAudioFileData]; + + [NSThread sleepForTimeInterval:0.5]; + }); + + describe(@"after attempting to play the file", ^{ + beforeEach(^{ + [mockAudioManager clearData]; + [testManager playNextWhenReady]; + }); + + it(@"should fail to send data", ^{ + expect(mockAudioManager.dataSinceClear.length).to(equal(0)); + expect(mockAudioManager.error.code).toEventually(equal(SDLAudioStreamManagerErrorNotConnected)); + }); + }); + }); + }); + + describe(@"after adding an audio file to the queue", ^{ beforeEach(^{ - mockAudioManager.audioConnected = NO; + mockAudioManager.audioConnected = YES; [testManager pushWithFileURL:testAudioFileURL]; [NSThread sleepForTimeInterval:0.5]; }); + it(@"should have a file in the queue", ^{ + expect(testManager.queue).toNot(beEmpty()); + }); + describe(@"after attempting to play the file", ^{ beforeEach(^{ [mockAudioManager clearData]; [testManager playNextWhenReady]; + + [NSThread sleepForTimeInterval:1.0]; }); - it(@"should fail to send data", ^{ - expect(mockAudioManager.dataSinceClear.length).to(equal(0)); - expect(mockAudioManager.fileError.code).to(equal(SDLAudioStreamManagerErrorNotConnected)); + it(@"should be sending data", ^{ + expect(testManager.isPlaying).toEventually(beTrue()); + expect(mockAudioManager.dataSinceClear.length).toEventually(equal(34380)); + + // Fails when it shouldn't, `weakself` goes to nil in `sdl_playNextWhenReady` + expect(mockAudioManager.finishedPlaying).toEventually(beTrue()); + }); + }); + + describe(@"after stopping the manager", ^{ + beforeEach(^{ + [testManager stop]; + }); + + it(@"should have an empty queue", ^{ + expect(testManager.queue).toEventually(beEmpty()); }); }); }); - describe(@"after adding an audio file to the queue", ^{ + describe(@"after adding an audio buffer to the queue", ^{ beforeEach(^{ mockAudioManager.audioConnected = YES; - [testManager pushWithFileURL:testAudioFileURL]; + [testManager pushWithData:testAudioFileData]; [NSThread sleepForTimeInterval:0.5]; }); @@ -59,7 +123,7 @@ expect(testManager.queue).toNot(beEmpty()); }); - describe(@"after attempting to play the file", ^{ + describe(@"after attempting to play the audio buffer", ^{ beforeEach(^{ [mockAudioManager clearData]; [testManager playNextWhenReady]; @@ -67,10 +131,10 @@ it(@"should be sending data", ^{ expect(testManager.isPlaying).toEventually(beTrue()); - expect(mockAudioManager.dataSinceClear.length).toEventually(equal(34380)); + expect(mockAudioManager.dataSinceClear.length).toEventually(equal(14838)); // Fails when it shouldn't, `weakself` goes to nil in `sdl_playNextWhenReady` -// expect(mockAudioManager.fileFinishedPlaying).toEventually(beTrue()); + expect(mockAudioManager.finishedPlaying).toEventually(beTrue()); }); }); @@ -80,7 +144,7 @@ }); it(@"should have an empty queue", ^{ - expect(testManager.queue).to(beEmpty()); + expect(testManager.queue).toEventually(beEmpty()); }); }); }); diff --git a/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.h b/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.h index 2717798a5..0b92d26d5 100644 --- a/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.h +++ b/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.h @@ -21,13 +21,14 @@ #pragma mark SDLStreamingAudioManagerType @property (assign, nonatomic, readonly, getter=isAudioConnected) BOOL audioConnected; - (BOOL)sendAudioData:(NSData *)audioData; - - (void)setAudioConnected:(BOOL)audioConnected; #pragma mark SDLAudioStreamManagerDelegate - (void)audioStreamManager:(SDLAudioStreamManager *)audioManager fileDidFinishPlaying:(SDLAudioFile *)file successfully:(BOOL)successfully; +- (void)audioStreamManager:(SDLAudioStreamManager *)audioManager dataBufferDidFinishPlayingSuccessfully:(BOOL)successfully; - (void)audioStreamManager:(SDLAudioStreamManager *)audioManager errorDidOccurForFile:(SDLAudioFile *)file error:(NSError *)error; -@property (assign, nonatomic, readonly) BOOL fileFinishedPlaying; -@property (strong, nonatomic, readonly) NSError *fileError; +- (void)audioStreamManager:(SDLAudioStreamManager *)audioManager errorDidOccurForDataBuffer:(NSError *)error; +@property (assign, nonatomic, readonly) BOOL finishedPlaying; +@property (strong, nonatomic, readonly) NSError *error; @end diff --git a/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.m b/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.m index 21d1d8732..6d03a7264 100644 --- a/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.m +++ b/SmartDeviceLinkTests/SDLStreamingAudioManagerMock.m @@ -14,8 +14,8 @@ @interface SDLStreamingAudioManagerMock() @property (strong, nonatomic) NSMutableData *mutableDataSinceClear; -@property (assign, nonatomic, readwrite) BOOL fileFinishedPlaying; -@property (strong, nonatomic, readwrite) NSError *fileError; +@property (assign, nonatomic, readwrite) BOOL finishedPlaying; +@property (strong, nonatomic, readwrite) NSError *error; @end @@ -35,8 +35,8 @@ - (void)clearData { _lastSentData = nil; _mutableDataSinceClear = nil; - _fileFinishedPlaying = NO; - _fileError = nil; + _finishedPlaying = NO; + _error = nil; } #pragma mark SDLStreamingAudioManagerType @@ -62,11 +62,19 @@ - (void)setAudioConnected:(BOOL)audioConnected { #pragma mark SDLAudioStreamManagerDelegate - (void)audioStreamManager:(SDLAudioStreamManager *)audioManager fileDidFinishPlaying:(SDLAudioFile *)file successfully:(BOOL)successfully { - _fileFinishedPlaying = successfully; + _finishedPlaying = successfully; +} + +- (void)audioStreamManager:(SDLAudioStreamManager *)audioManager dataBufferDidFinishPlayingSuccessfully:(BOOL)successfully { + _finishedPlaying = successfully; } - (void)audioStreamManager:(SDLAudioStreamManager *)audioManager errorDidOccurForFile:(SDLAudioFile *)file error:(NSError *)error { - _fileError = error; + _error = error; +} + +- (void)audioStreamManager:(SDLAudioStreamManager *)audioManager errorDidOccurForDataBuffer:(NSError *)error { + _error = error; } @end