Skip to content

Commit

Permalink
Merge pull request #1289 from smartdevicelink/feature/issue_1275_audi…
Browse files Browse the repository at this point in the history
…ostreammanager_push_data_buffer

Support Pushing a PCM Audio Data Buffer on SDLAudioStreamManager
  • Loading branch information
joeljfischer committed Jun 5, 2019
2 parents 03c394a + fbb9481 commit 762a058
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 36 deletions.
32 changes: 30 additions & 2 deletions SmartDeviceLink/SDLAudioFile.h
Expand Up @@ -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
9 changes: 9 additions & 0 deletions SmartDeviceLink/SDLAudioFile.m
Expand Up @@ -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];
Expand Down
16 changes: 16 additions & 0 deletions SmartDeviceLink/SDLAudioStreamManager.h
Expand Up @@ -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.
Expand Down
65 changes: 52 additions & 13 deletions SmartDeviceLink/SDLAudioStreamManager.m
Expand Up @@ -46,10 +46,22 @@ - (instancetype)initWithManager:(id<SDLStreamingAudioManagerType>)streamManager
return self;
}

- (void)stop {
dispatch_async(_audioQueue, ^{
self.shouldPlayWhenReady = NO;
[self.mutableQueue removeAllObjects];
});
}

#pragma mark - Getters

- (NSArray<SDLFile *> *)queue {
return [_mutableQueue copy];
}

#pragma mark - Pushing to the Queue
#pragma mark Files

- (void)pushWithFileURL:(NSURL *)fileURL {
dispatch_async(_audioQueue, ^{
[self sdl_pushWithContentsOfURL:fileURL];
Expand Down Expand Up @@ -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];
Expand All @@ -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
20 changes: 19 additions & 1 deletion SmartDeviceLink/SDLAudioStreamManagerDelegate.h
Expand Up @@ -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
4 changes: 3 additions & 1 deletion SmartDeviceLink/SDLStreamingMediaManager.h
Expand Up @@ -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
*
Expand Down
84 changes: 74 additions & 10 deletions SmartDeviceLinkTests/DevAPISpecs/SDLAudioStreamManagerSpec.m
Expand Up @@ -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];
Expand All @@ -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];
});
Expand All @@ -59,18 +123,18 @@
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];
});

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());
});
});

Expand All @@ -80,7 +144,7 @@
});

it(@"should have an empty queue", ^{
expect(testManager.queue).to(beEmpty());
expect(testManager.queue).toEventually(beEmpty());
});
});
});
Expand Down
7 changes: 4 additions & 3 deletions SmartDeviceLinkTests/SDLStreamingAudioManagerMock.h
Expand Up @@ -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

0 comments on commit 762a058

Please sign in to comment.