Now properly supports sample format changing
Sample format can now change dynamically at play time, and the player will resample it as necessary, extrapolating edges between changes to reduce the potential for gaps. Currently supported formats for this: - FLAC - Ogg Vorbis - Any format supported by FFmpeg, such as MP3 or AAC Signed-off-by: Christopher Snowhill <kode54@gmail.com>CQTexperiment
parent
0b8a659bc2
commit
477feaab1d
|
@ -66,8 +66,6 @@
|
||||||
- (BOOL)endOfInputReached;
|
- (BOOL)endOfInputReached;
|
||||||
- (BOOL)setTrack:(NSURL *)track;
|
- (BOOL)setTrack:(NSURL *)track;
|
||||||
|
|
||||||
- (void)inputFormatDidChange:(AudioStreamBasicDescription)format inputConfig:(uint32_t)inputConfig;
|
|
||||||
|
|
||||||
- (BOOL)isRunning;
|
- (BOOL)isRunning;
|
||||||
|
|
||||||
- (id)controller;
|
- (id)controller;
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
inputFormat = [inputNode nodeFormat];
|
inputFormat = [inputNode nodeFormat];
|
||||||
if([properties valueForKey:@"channelConfig"])
|
if([properties valueForKey:@"channelConfig"])
|
||||||
inputChannelConfig = [[properties valueForKey:@"channelConfig"] intValue];
|
inputChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
|
||||||
|
|
||||||
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
|
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
|
||||||
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
|
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
|
|
||||||
inputFormat = [inputNode nodeFormat];
|
inputFormat = [inputNode nodeFormat];
|
||||||
if([properties valueForKey:@"channelConfig"])
|
if([properties valueForKey:@"channelConfig"])
|
||||||
inputChannelConfig = [[properties valueForKey:@"channelConfig"] intValue];
|
inputChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
|
||||||
|
|
||||||
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
|
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
|
||||||
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
|
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
|
|
||||||
inputFormat = [inputNode nodeFormat];
|
inputFormat = [inputNode nodeFormat];
|
||||||
if([properties valueForKey:@"channelConfig"])
|
if([properties valueForKey:@"channelConfig"])
|
||||||
inputChannelConfig = [[properties valueForKey:@"channelConfig"] intValue];
|
inputChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
|
||||||
|
|
||||||
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
|
outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
|
||||||
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
|
outputFormat.mBytesPerFrame = ((outputFormat.mBitsPerChannel + 7) / 8) * outputFormat.mChannelsPerFrame;
|
||||||
|
@ -193,10 +193,6 @@
|
||||||
[controller launchOutputThread];
|
[controller launchOutputThread];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)inputFormatDidChange:(AudioStreamBasicDescription)format inputConfig:(uint32_t)inputConfig {
|
|
||||||
DLog(@"FORMAT DID CHANGE!");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (InputNode *)inputNode {
|
- (InputNode *)inputNode {
|
||||||
return inputNode;
|
return inputNode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
BOOL inAdder;
|
BOOL inAdder;
|
||||||
BOOL inRemover;
|
BOOL inRemover;
|
||||||
|
BOOL inPeeker;
|
||||||
BOOL stopping;
|
BOOL stopping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
- (void)addChunk:(AudioChunk *)chunk;
|
- (void)addChunk:(AudioChunk *)chunk;
|
||||||
- (AudioChunk *)removeSamples:(size_t)maxFrameCount;
|
- (AudioChunk *)removeSamples:(size_t)maxFrameCount;
|
||||||
|
|
||||||
|
- (BOOL)peekFormat:(nonnull AudioStreamBasicDescription *)format channelConfig:(nonnull uint32_t *)config;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
inAdder = NO;
|
inAdder = NO;
|
||||||
inRemover = NO;
|
inRemover = NO;
|
||||||
|
inPeeker = NO;
|
||||||
stopping = NO;
|
stopping = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
stopping = YES;
|
stopping = YES;
|
||||||
while(inAdder || inRemover) {
|
while(inAdder || inRemover || inPeeker) {
|
||||||
usleep(500);
|
usleep(500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,4 +97,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)peekFormat:(AudioStreamBasicDescription *)format channelConfig:(uint32_t *)config {
|
||||||
|
if(stopping) return NO;
|
||||||
|
@synchronized(chunkList) {
|
||||||
|
if([chunkList count]) {
|
||||||
|
AudioChunk *chunk = [chunkList objectAtIndex:0];
|
||||||
|
*format = [chunk format];
|
||||||
|
*config = [chunk channelConfig];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -71,6 +71,12 @@
|
||||||
uint32_t inputChannelConfig;
|
uint32_t inputChannelConfig;
|
||||||
uint32_t outputChannelConfig;
|
uint32_t outputChannelConfig;
|
||||||
|
|
||||||
|
BOOL streamFormatChanged;
|
||||||
|
AudioStreamBasicDescription newInputFormat;
|
||||||
|
uint32_t newInputChannelConfig;
|
||||||
|
|
||||||
|
AudioChunk *lastChunkIn;
|
||||||
|
|
||||||
AudioStreamBasicDescription previousOutputFormat;
|
AudioStreamBasicDescription previousOutputFormat;
|
||||||
uint32_t previousOutputConfig;
|
uint32_t previousOutputConfig;
|
||||||
AudioStreamBasicDescription rememberedInputFormat;
|
AudioStreamBasicDescription rememberedInputFormat;
|
||||||
|
|
|
@ -440,6 +440,10 @@ static void convert_be_to_le(uint8_t *buffer, size_t bitsPerSample, size_t bytes
|
||||||
[self cleanUp];
|
[self cleanUp];
|
||||||
[self setupWithInputFormat:rememberedInputFormat withInputConfig:rememberedInputConfig outputFormat:outputFormat outputConfig:outputChannelConfig isLossless:rememberedLossless];
|
[self setupWithInputFormat:rememberedInputFormat withInputConfig:rememberedInputConfig outputFormat:outputFormat outputConfig:outputChannelConfig isLossless:rememberedLossless];
|
||||||
continue;
|
continue;
|
||||||
|
} else if(streamFormatChanged) {
|
||||||
|
[self cleanUp];
|
||||||
|
[self setupWithInputFormat:newInputFormat withInputConfig:newInputChannelConfig outputFormat:outputFormat outputConfig:outputChannelConfig isLossless:rememberedLossless];
|
||||||
|
continue;
|
||||||
} else
|
} else
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -452,6 +456,7 @@ static void convert_be_to_le(uint8_t *buffer, size_t bitsPerSample, size_t bytes
|
||||||
int amountReadFromFC;
|
int amountReadFromFC;
|
||||||
int amountRead = 0;
|
int amountRead = 0;
|
||||||
int amountToSkip;
|
int amountToSkip;
|
||||||
|
int amountToIgnorePostExtrapolated = 0;
|
||||||
|
|
||||||
if(stopping)
|
if(stopping)
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -498,14 +503,36 @@ tryagain:
|
||||||
|
|
||||||
ssize_t bytesReadFromInput = 0;
|
ssize_t bytesReadFromInput = 0;
|
||||||
|
|
||||||
while(bytesReadFromInput < amountToWrite && !stopping && [self shouldContinue] == YES && [self endOfStream] == NO) {
|
while(bytesReadFromInput < amountToWrite && !stopping && !streamFormatChanged && [self shouldContinue] == YES && [self endOfStream] == NO) {
|
||||||
|
AudioStreamBasicDescription inf;
|
||||||
|
uint32_t config;
|
||||||
|
if([self peekFormat:&inf channelConfig:&config]) {
|
||||||
|
if(config != inputChannelConfig || memcmp(&inf, &inputFormat, sizeof(inf)) != 0) {
|
||||||
|
if(inputChannelConfig == 0 && memcmp(&inf, &inputFormat, sizeof(inf)) == 0) {
|
||||||
|
inputChannelConfig = config;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
newInputFormat = inf;
|
||||||
|
newInputChannelConfig = config;
|
||||||
|
streamFormatChanged = YES;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AudioChunk *chunk = [self readChunk:((amountToWrite - bytesReadFromInput) / inputFormat.mBytesPerPacket)];
|
AudioChunk *chunk = [self readChunk:((amountToWrite - bytesReadFromInput) / inputFormat.mBytesPerPacket)];
|
||||||
AudioStreamBasicDescription inf = [chunk format];
|
inf = [chunk format];
|
||||||
size_t frameCount = [chunk frameCount];
|
size_t frameCount = [chunk frameCount];
|
||||||
|
config = [chunk channelConfig];
|
||||||
size_t bytesRead = frameCount * inf.mBytesPerPacket;
|
size_t bytesRead = frameCount * inf.mBytesPerPacket;
|
||||||
if(frameCount) {
|
if(frameCount) {
|
||||||
NSData *samples = [chunk removeSamples:frameCount];
|
NSData *samples = [chunk removeSamples:frameCount];
|
||||||
memcpy(inputBuffer + bytesReadFromInput + amountToSkip, [samples bytes], bytesRead);
|
memcpy(inputBuffer + bytesReadFromInput + amountToSkip, [samples bytes], bytesRead);
|
||||||
|
lastChunkIn = [[AudioChunk alloc] init];
|
||||||
|
[lastChunkIn setFormat:inf];
|
||||||
|
[lastChunkIn setChannelConfig:config];
|
||||||
|
[lastChunkIn setLossless:[chunk lossless]];
|
||||||
|
[lastChunkIn assignSamples:[samples bytes] frameCount:frameCount];
|
||||||
}
|
}
|
||||||
bytesReadFromInput += bytesRead;
|
bytesReadFromInput += bytesRead;
|
||||||
if(!frameCount) {
|
if(!frameCount) {
|
||||||
|
@ -518,7 +545,7 @@ tryagain:
|
||||||
|
|
||||||
// Pad end of track with input format silence
|
// Pad end of track with input format silence
|
||||||
|
|
||||||
if(stopping || [self shouldContinue] == NO || [self endOfStream] == YES) {
|
if(stopping || streamFormatChanged || [self shouldContinue] == NO || [self endOfStream] == YES) {
|
||||||
if(!skipResampler && !is_postextrapolated_) {
|
if(!skipResampler && !is_postextrapolated_) {
|
||||||
if(dsd2pcm) {
|
if(dsd2pcm) {
|
||||||
amountToSkip = dsd2pcmLatency * inputFormat.mBytesPerPacket;
|
amountToSkip = dsd2pcmLatency * inputFormat.mBytesPerPacket;
|
||||||
|
@ -532,6 +559,24 @@ tryagain:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t bitsPerSample = inputFormat.mBitsPerChannel;
|
||||||
|
BOOL isBigEndian = !!(inputFormat.mFormatFlags & kAudioFormatFlagIsBigEndian);
|
||||||
|
|
||||||
|
if(!bytesReadFromInput && streamFormatChanged && !skipResampler && is_postextrapolated_ < 2) {
|
||||||
|
AudioChunk *chunk = lastChunkIn;
|
||||||
|
lastChunkIn = nil;
|
||||||
|
AudioStreamBasicDescription inf = [chunk format];
|
||||||
|
size_t frameCount = [chunk frameCount];
|
||||||
|
size_t bytesRead = frameCount * inf.mBytesPerPacket;
|
||||||
|
if(frameCount) {
|
||||||
|
amountToIgnorePostExtrapolated = (int)frameCount;
|
||||||
|
NSData *samples = [chunk removeSamples:frameCount];
|
||||||
|
memcpy(inputBuffer, [samples bytes], bytesRead);
|
||||||
|
}
|
||||||
|
bytesReadFromInput += bytesRead;
|
||||||
|
amountToSkip = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if(!bytesReadFromInput) {
|
if(!bytesReadFromInput) {
|
||||||
convertEntered = NO;
|
convertEntered = NO;
|
||||||
return amountRead;
|
return amountRead;
|
||||||
|
@ -544,8 +589,7 @@ tryagain:
|
||||||
dsdLatencyEaten = (int)ceil(dsd2pcmLatency * sampleRatio);
|
dsdLatencyEaten = (int)ceil(dsd2pcmLatency * sampleRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bytesReadFromInput &&
|
if(bytesReadFromInput && isBigEndian) {
|
||||||
(inputFormat.mFormatFlags & kAudioFormatFlagIsBigEndian)) {
|
|
||||||
// Time for endian swap!
|
// Time for endian swap!
|
||||||
convert_be_to_le(inputBuffer, inputFormat.mBitsPerChannel, bytesReadFromInput);
|
convert_be_to_le(inputBuffer, inputFormat.mBitsPerChannel, bytesReadFromInput);
|
||||||
}
|
}
|
||||||
|
@ -560,7 +604,6 @@ tryagain:
|
||||||
|
|
||||||
if(bytesReadFromInput && !isFloat) {
|
if(bytesReadFromInput && !isFloat) {
|
||||||
float gain = 1.0;
|
float gain = 1.0;
|
||||||
size_t bitsPerSample = inputFormat.mBitsPerChannel;
|
|
||||||
if(bitsPerSample == 1) {
|
if(bitsPerSample == 1) {
|
||||||
samplesRead = bytesReadFromInput / inputFormat.mBytesPerPacket;
|
samplesRead = bytesReadFromInput / inputFormat.mBytesPerPacket;
|
||||||
convert_dsd_to_f32(inputBuffer + bytesReadFromInput, inputBuffer, samplesRead, inputFormat.mChannelsPerFrame, dsd2pcm);
|
convert_dsd_to_f32(inputBuffer + bytesReadFromInput, inputBuffer, samplesRead, inputFormat.mChannelsPerFrame, dsd2pcm);
|
||||||
|
@ -699,7 +742,7 @@ tryagain:
|
||||||
|
|
||||||
// Input now contains bytesReadFromInput worth of floats, in the input sample rate
|
// Input now contains bytesReadFromInput worth of floats, in the input sample rate
|
||||||
inpSize = bytesReadFromInput;
|
inpSize = bytesReadFromInput;
|
||||||
inpOffset = 0;
|
inpOffset = amountToIgnorePostExtrapolated * floatFormat.mBytesPerPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(inpOffset != inpSize && floatOffset == floatSize) {
|
if(inpOffset != inpSize && floatOffset == floatSize) {
|
||||||
|
@ -948,6 +991,7 @@ static float db_to_scale(float db) {
|
||||||
convertEntered = NO;
|
convertEntered = NO;
|
||||||
paused = NO;
|
paused = NO;
|
||||||
outputFormatChanged = NO;
|
outputFormatChanged = NO;
|
||||||
|
streamFormatChanged = NO;
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
nodeFormat = propertiesToASBD(properties);
|
nodeFormat = propertiesToASBD(properties);
|
||||||
if([properties valueForKey:@"channelConfig"])
|
if([properties valueForKey:@"channelConfig"])
|
||||||
nodeChannelConfig = [[properties valueForKey:@"channelConfig"] intValue];
|
nodeChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
|
||||||
nodeLossless = [[properties valueForKey:@"encoding"] isEqualToString:@"lossless"];
|
nodeLossless = [[properties valueForKey:@"encoding"] isEqualToString:@"lossless"];
|
||||||
|
|
||||||
shouldContinue = YES;
|
shouldContinue = YES;
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
|
|
||||||
nodeFormat = propertiesToASBD(properties);
|
nodeFormat = propertiesToASBD(properties);
|
||||||
if([properties valueForKey:@"channelConfig"])
|
if([properties valueForKey:@"channelConfig"])
|
||||||
nodeChannelConfig = [[properties valueForKey:@"channelConfig"] intValue];
|
nodeChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
|
||||||
nodeLossless = [[properties valueForKey:@"encoding"] isEqualToString:@"lossless"];
|
nodeLossless = [[properties valueForKey:@"encoding"] isEqualToString:@"lossless"];
|
||||||
|
|
||||||
[self registerObservers];
|
[self registerObservers];
|
||||||
|
@ -105,7 +105,14 @@
|
||||||
DLog(@"Input format changed");
|
DLog(@"Input format changed");
|
||||||
// Converter may need resetting, it'll do that when it reaches the new chunks
|
// Converter may need resetting, it'll do that when it reaches the new chunks
|
||||||
NSDictionary *properties = [decoder properties];
|
NSDictionary *properties = [decoder properties];
|
||||||
|
|
||||||
|
int bitsPerSample = [[properties objectForKey:@"bitsPerSample"] intValue];
|
||||||
|
int channels = [[properties objectForKey:@"channels"] intValue];
|
||||||
|
|
||||||
|
bytesPerFrame = ((bitsPerSample + 7) / 8) * channels;
|
||||||
|
|
||||||
nodeFormat = propertiesToASBD(properties);
|
nodeFormat = propertiesToASBD(properties);
|
||||||
|
nodeChannelConfig = [[properties valueForKey:@"channelConfig"] unsignedIntValue];
|
||||||
nodeLossless = [[properties valueForKey:@"encoding"] isEqualToString:@"lossless"];
|
nodeLossless = [[properties valueForKey:@"encoding"] isEqualToString:@"lossless"];
|
||||||
} else if([keyPath isEqual:@"metadata"]) {
|
} else if([keyPath isEqual:@"metadata"]) {
|
||||||
// Inform something of metadata change
|
// Inform something of metadata change
|
||||||
|
@ -114,7 +121,8 @@
|
||||||
|
|
||||||
- (void)process {
|
- (void)process {
|
||||||
int amountInBuffer = 0;
|
int amountInBuffer = 0;
|
||||||
void *inputBuffer = malloc(CHUNK_SIZE);
|
int bytesInBuffer = 0;
|
||||||
|
void *inputBuffer = malloc(CHUNK_SIZE * 18); // Maximum 18 channels, dunno what we'll receive
|
||||||
|
|
||||||
BOOL shouldClose = YES;
|
BOOL shouldClose = YES;
|
||||||
BOOL seekError = NO;
|
BOOL seekError = NO;
|
||||||
|
@ -142,13 +150,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if(amountInBuffer < CHUNK_SIZE) {
|
if(amountInBuffer < CHUNK_SIZE) {
|
||||||
int framesToRead = (CHUNK_SIZE - amountInBuffer) / bytesPerFrame;
|
int framesToRead = CHUNK_SIZE - amountInBuffer;
|
||||||
int framesRead = [decoder readAudio:((char *)inputBuffer) + amountInBuffer frames:framesToRead];
|
int framesRead = [decoder readAudio:((char *)inputBuffer) + bytesInBuffer frames:framesToRead];
|
||||||
|
|
||||||
if(framesRead > 0 && !seekError) {
|
if(framesRead > 0 && !seekError) {
|
||||||
amountInBuffer += (framesRead * bytesPerFrame);
|
amountInBuffer += framesRead;
|
||||||
[self writeData:inputBuffer amount:amountInBuffer];
|
bytesInBuffer += framesRead * bytesPerFrame;
|
||||||
|
[self writeData:inputBuffer amount:bytesInBuffer];
|
||||||
amountInBuffer = 0;
|
amountInBuffer = 0;
|
||||||
|
bytesInBuffer = 0;
|
||||||
} else {
|
} else {
|
||||||
if(initialBufferFilled == NO) {
|
if(initialBufferFilled == NO) {
|
||||||
[controller initialBufferFilled:self];
|
[controller initialBufferFilled:self];
|
||||||
|
|
|
@ -32,33 +32,35 @@
|
||||||
uint32_t nodeChannelConfig;
|
uint32_t nodeChannelConfig;
|
||||||
BOOL nodeLossless;
|
BOOL nodeLossless;
|
||||||
}
|
}
|
||||||
- (id)initWithController:(id)c previous:(id)p;
|
- (id _Nullable)initWithController:(id _Nonnull)c previous:(id _Nullable)p;
|
||||||
|
|
||||||
- (void)writeData:(const void *)ptr amount:(size_t)a;
|
- (void)writeData:(const void *_Nonnull)ptr amount:(size_t)a;
|
||||||
- (AudioChunk *)readChunk:(size_t)maxFrames;
|
- (AudioChunk *_Nonnull)readChunk:(size_t)maxFrames;
|
||||||
|
|
||||||
|
- (BOOL)peekFormat:(AudioStreamBasicDescription *_Nonnull)format channelConfig:(uint32_t *_Nonnull)config;
|
||||||
|
|
||||||
- (void)process; // Should be overwriten by subclass
|
- (void)process; // Should be overwriten by subclass
|
||||||
- (void)threadEntry:(id)arg;
|
- (void)threadEntry:(id _Nullable)arg;
|
||||||
|
|
||||||
- (void)launchThread;
|
- (void)launchThread;
|
||||||
|
|
||||||
- (void)setShouldReset:(BOOL)s;
|
- (void)setShouldReset:(BOOL)s;
|
||||||
- (BOOL)shouldReset;
|
- (BOOL)shouldReset;
|
||||||
|
|
||||||
- (void)setPreviousNode:(id)p;
|
- (void)setPreviousNode:(id _Nullable)p;
|
||||||
- (id)previousNode;
|
- (id _Nullable)previousNode;
|
||||||
|
|
||||||
- (BOOL)shouldContinue;
|
- (BOOL)shouldContinue;
|
||||||
- (void)setShouldContinue:(BOOL)s;
|
- (void)setShouldContinue:(BOOL)s;
|
||||||
|
|
||||||
- (ChunkList *)buffer;
|
- (ChunkList *_Nonnull)buffer;
|
||||||
- (void)resetBuffer; // WARNING! DANGER WILL ROBINSON!
|
- (void)resetBuffer; // WARNING! DANGER WILL ROBINSON!
|
||||||
|
|
||||||
- (AudioStreamBasicDescription)nodeFormat;
|
- (AudioStreamBasicDescription)nodeFormat;
|
||||||
- (uint32_t)nodeChannelConfig;
|
- (uint32_t)nodeChannelConfig;
|
||||||
- (BOOL)nodeLossless;
|
- (BOOL)nodeLossless;
|
||||||
|
|
||||||
- (Semaphore *)semaphore;
|
- (Semaphore *_Nonnull)semaphore;
|
||||||
|
|
||||||
//-(void)resetBuffer;
|
//-(void)resetBuffer;
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)peekFormat:(nonnull AudioStreamBasicDescription *)format channelConfig:(nonnull uint32_t *)config {
|
||||||
|
[accessLock lock];
|
||||||
|
|
||||||
|
BOOL ret = [[previousNode buffer] peekFormat:format channelConfig:config];
|
||||||
|
|
||||||
|
[accessLock unlock];
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
- (AudioChunk *)readChunk:(size_t)maxFrames {
|
- (AudioChunk *)readChunk:(size_t)maxFrames {
|
||||||
[accessLock lock];
|
[accessLock lock];
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
@interface CogDecoderMulti : NSObject <CogDecoder> {
|
@interface CogDecoderMulti : NSObject <CogDecoder> {
|
||||||
NSArray *theDecoders;
|
NSArray *theDecoders;
|
||||||
id<CogDecoder> theDecoder;
|
id<CogDecoder> theDecoder;
|
||||||
NSMutableArray *cachedObservers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithDecoders:(NSArray *)decoders;
|
- (id)initWithDecoders:(NSArray *)decoders;
|
||||||
|
|
|
@ -31,6 +31,15 @@ NSArray *sortClassesByPriority(NSArray *theClasses) {
|
||||||
return sortedClasses;
|
return sortedClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@interface CogDecoderMulti (Private)
|
||||||
|
- (void)registerObservers;
|
||||||
|
- (void)removeObservers;
|
||||||
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||||
|
ofObject:(id)object
|
||||||
|
change:(NSDictionary *)change
|
||||||
|
context:(void *)context;
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation CogDecoderMulti
|
@implementation CogDecoderMulti
|
||||||
|
|
||||||
+ (NSArray *)mimeTypes {
|
+ (NSArray *)mimeTypes {
|
||||||
|
@ -54,7 +63,6 @@ NSArray *sortClassesByPriority(NSArray *theClasses) {
|
||||||
if(self) {
|
if(self) {
|
||||||
theDecoders = sortClassesByPriority(decoders);
|
theDecoders = sortClassesByPriority(decoders);
|
||||||
theDecoder = nil;
|
theDecoder = nil;
|
||||||
cachedObservers = [[NSMutableArray alloc] init];
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
@ -73,17 +81,10 @@ NSArray *sortClassesByPriority(NSArray *theClasses) {
|
||||||
for(NSString *classString in theDecoders) {
|
for(NSString *classString in theDecoders) {
|
||||||
Class decoder = NSClassFromString(classString);
|
Class decoder = NSClassFromString(classString);
|
||||||
theDecoder = [[decoder alloc] init];
|
theDecoder = [[decoder alloc] init];
|
||||||
for(NSDictionary *obsItem in cachedObservers) {
|
[self registerObservers];
|
||||||
[theDecoder addObserver:[obsItem objectForKey:@"observer"]
|
|
||||||
forKeyPath:[obsItem objectForKey:@"keyPath"]
|
|
||||||
options:[[obsItem objectForKey:@"options"] unsignedIntegerValue]
|
|
||||||
context:(__bridge void *)([obsItem objectForKey:@"context"])];
|
|
||||||
}
|
|
||||||
if([theDecoder open:source])
|
if([theDecoder open:source])
|
||||||
return YES;
|
return YES;
|
||||||
for(NSDictionary *obsItem in cachedObservers) {
|
[self removeObservers];
|
||||||
[theDecoder removeObserver:[obsItem objectForKey:@"observer"] forKeyPath:[obsItem objectForKey:@"keyPath"]];
|
|
||||||
}
|
|
||||||
if([source seekable])
|
if([source seekable])
|
||||||
[source seek:0 whence:SEEK_SET];
|
[source seek:0 whence:SEEK_SET];
|
||||||
}
|
}
|
||||||
|
@ -98,43 +99,40 @@ NSArray *sortClassesByPriority(NSArray *theClasses) {
|
||||||
|
|
||||||
- (void)close {
|
- (void)close {
|
||||||
if(theDecoder != nil) {
|
if(theDecoder != nil) {
|
||||||
for(NSDictionary *obsItem in cachedObservers) {
|
[self removeObservers];
|
||||||
[theDecoder removeObserver:[obsItem objectForKey:@"observer"] forKeyPath:[obsItem objectForKey:@"keyPath"]];
|
|
||||||
}
|
|
||||||
[cachedObservers removeAllObjects];
|
|
||||||
[theDecoder close];
|
[theDecoder close];
|
||||||
theDecoder = nil;
|
theDecoder = nil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)registerObservers {
|
||||||
|
[theDecoder addObserver:self
|
||||||
|
forKeyPath:@"properties"
|
||||||
|
options:(NSKeyValueObservingOptionNew)
|
||||||
|
context:NULL];
|
||||||
|
|
||||||
|
[theDecoder addObserver:self
|
||||||
|
forKeyPath:@"metadata"
|
||||||
|
options:(NSKeyValueObservingOptionNew)
|
||||||
|
context:NULL];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)removeObservers {
|
||||||
|
[theDecoder removeObserver:self forKeyPath:@"properties"];
|
||||||
|
[theDecoder removeObserver:self forKeyPath:@"metadata"];
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)setTrack:(NSURL *)track {
|
- (BOOL)setTrack:(NSURL *)track {
|
||||||
if(theDecoder != nil && [theDecoder respondsToSelector:@selector(setTrack:)]) return [theDecoder setTrack:track];
|
if(theDecoder != nil && [theDecoder respondsToSelector:@selector(setTrack:)]) return [theDecoder setTrack:track];
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* By the current design, the core adds its observers to decoders before they are opened */
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||||
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
|
ofObject:(id)object
|
||||||
if(context != nil) {
|
change:(NSDictionary *)change
|
||||||
[cachedObservers addObject:[NSDictionary dictionaryWithObjectsAndKeys:observer, @"observer", keyPath, @"keyPath", @(options), @"options", context, @"context", nil]];
|
context:(void *)context {
|
||||||
} else {
|
[self willChangeValueForKey:keyPath];
|
||||||
[cachedObservers addObject:[NSDictionary dictionaryWithObjectsAndKeys:observer, @"observer", keyPath, @"keyPath", @(options), @"options", nil]];
|
[self didChangeValueForKey:keyPath];
|
||||||
}
|
|
||||||
if(theDecoder) {
|
|
||||||
[theDecoder addObserver:observer forKeyPath:keyPath options:options context:context];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* And this is currently called after the decoder is closed */
|
|
||||||
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
|
|
||||||
for(NSDictionary *obsItem in cachedObservers) {
|
|
||||||
if([obsItem objectForKey:@"observer"] == observer && [keyPath isEqualToString:[obsItem objectForKey:@"keyPath"]]) {
|
|
||||||
[cachedObservers removeObject:obsItem];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(theDecoder) {
|
|
||||||
[theDecoder removeObserver:observer forKeyPath:keyPath];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -666,6 +666,8 @@ default_device_changed(AudioObjectID inObjectID, UInt32 inNumberAddresses, const
|
||||||
if(logFile) {
|
if(logFile) {
|
||||||
fwrite(inputData->mBuffers[0].mData, 1, inputData->mBuffers[0].mDataByteSize, logFile);
|
fwrite(inputData->mBuffers[0].mData, 1, inputData->mBuffers[0].mDataByteSize, logFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// memset(inputData->mBuffers[0].mData, 0, inputData->mBuffers[0].mDataByteSize);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
inputData->mBuffers[0].mNumberChannels = channels;
|
inputData->mBuffers[0].mNumberChannels = channels;
|
||||||
|
|
|
@ -391,7 +391,7 @@ static SInt64 getSizeProc(void *clientData) {
|
||||||
- (NSDictionary *)properties {
|
- (NSDictionary *)properties {
|
||||||
return [NSDictionary dictionaryWithObjectsAndKeys:
|
return [NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithInt:channels], @"channels",
|
[NSNumber numberWithInt:channels], @"channels",
|
||||||
[NSNumber numberWithInt:channelConfig], @"channelConfig",
|
[NSNumber numberWithUnsignedInt:channelConfig], @"channelConfig",
|
||||||
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
||||||
[NSNumber numberWithBool:floatingPoint], @"floatingPoint",
|
[NSNumber numberWithBool:floatingPoint], @"floatingPoint",
|
||||||
[NSNumber numberWithInt:bitrate], @"bitrate",
|
[NSNumber numberWithInt:bitrate], @"bitrate",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
BOOL embedded;
|
BOOL embedded;
|
||||||
BOOL noFragment;
|
BOOL noFragment;
|
||||||
|
BOOL observersAdded;
|
||||||
NSURL *baseURL;
|
NSURL *baseURL;
|
||||||
|
|
||||||
CueSheet *cuesheet;
|
CueSheet *cuesheet;
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
NSDictionary *fileMetadata;
|
NSDictionary *fileMetadata;
|
||||||
|
|
||||||
noFragment = NO;
|
noFragment = NO;
|
||||||
|
observersAdded = NO;
|
||||||
|
|
||||||
NSString *ext = [url pathExtension];
|
NSString *ext = [url pathExtension];
|
||||||
if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) {
|
if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) {
|
||||||
|
@ -143,6 +144,8 @@
|
||||||
|
|
||||||
decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES];
|
decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES];
|
||||||
|
|
||||||
|
[self registerObservers];
|
||||||
|
|
||||||
if(![decoder open:source]) {
|
if(![decoder open:source]) {
|
||||||
ALog(@"Could not open cuesheet decoder");
|
ALog(@"Could not open cuesheet decoder");
|
||||||
return NO;
|
return NO;
|
||||||
|
@ -166,8 +169,40 @@
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)registerObservers {
|
||||||
|
DLog(@"REGISTERING OBSERVERS");
|
||||||
|
[decoder addObserver:self
|
||||||
|
forKeyPath:@"properties"
|
||||||
|
options:(NSKeyValueObservingOptionNew)
|
||||||
|
context:NULL];
|
||||||
|
|
||||||
|
[decoder addObserver:self
|
||||||
|
forKeyPath:@"metadata"
|
||||||
|
options:(NSKeyValueObservingOptionNew)
|
||||||
|
context:NULL];
|
||||||
|
|
||||||
|
observersAdded = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)removeObservers {
|
||||||
|
if(observersAdded) {
|
||||||
|
[decoder removeObserver:self forKeyPath:@"properties"];
|
||||||
|
[decoder removeObserver:self forKeyPath:@"metadata"];
|
||||||
|
observersAdded = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||||
|
ofObject:(id)object
|
||||||
|
change:(NSDictionary *)change
|
||||||
|
context:(void *)context {
|
||||||
|
[self willChangeValueForKey:keyPath];
|
||||||
|
[self didChangeValueForKey:keyPath];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)close {
|
- (void)close {
|
||||||
if(decoder) {
|
if(decoder) {
|
||||||
|
[self removeObservers];
|
||||||
[decoder close];
|
[decoder close];
|
||||||
decoder = nil;
|
decoder = nil;
|
||||||
}
|
}
|
||||||
|
|
|
@ -575,6 +575,20 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
|
||||||
bytesRead += toConsume;
|
bytesRead += toConsume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _channels = codecCtx->channels;
|
||||||
|
uint32_t _channelConfig = (uint32_t)codecCtx->channel_layout;
|
||||||
|
float _frequency = codecCtx->sample_rate;
|
||||||
|
|
||||||
|
if(_channels != channels ||
|
||||||
|
_channelConfig != channelConfig ||
|
||||||
|
_frequency != frequency) {
|
||||||
|
channels = _channels;
|
||||||
|
channelConfig = _channelConfig;
|
||||||
|
frequency = _frequency;
|
||||||
|
[self willChangeValueForKey:@"properties"];
|
||||||
|
[self didChangeValueForKey:@"properties"];
|
||||||
|
}
|
||||||
|
|
||||||
int framesReadNow = bytesRead / frameSize;
|
int framesReadNow = bytesRead / frameSize;
|
||||||
if(totalFrames && (framesRead + framesReadNow > totalFrames))
|
if(totalFrames && (framesRead + framesReadNow > totalFrames))
|
||||||
framesReadNow = (int)(totalFrames - framesRead);
|
framesReadNow = (int)(totalFrames - framesRead);
|
||||||
|
@ -617,7 +631,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
|
||||||
- (NSDictionary *)properties {
|
- (NSDictionary *)properties {
|
||||||
return [NSDictionary dictionaryWithObjectsAndKeys:
|
return [NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithInt:channels], @"channels",
|
[NSNumber numberWithInt:channels], @"channels",
|
||||||
[NSNumber numberWithInt:channelConfig], @"channelConfig",
|
[NSNumber numberWithUnsignedInt:channelConfig], @"channelConfig",
|
||||||
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
||||||
[NSNumber numberWithBool:(bitsPerSample == 8)], @"Unsigned",
|
[NSNumber numberWithBool:(bitsPerSample == 8)], @"Unsigned",
|
||||||
[NSNumber numberWithFloat:frequency], @"sampleRate",
|
[NSNumber numberWithFloat:frequency], @"sampleRate",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
long fileSize;
|
long fileSize;
|
||||||
|
|
||||||
BOOL hasStreamInfo;
|
BOOL hasStreamInfo;
|
||||||
|
BOOL streamOpened;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setSource:(id<CogSource>)s;
|
- (void)setSource:(id<CogSource>)s;
|
||||||
|
|
|
@ -99,6 +99,23 @@ FLAC__StreamDecoderLengthStatus LengthCallback(const FLAC__StreamDecoder *decode
|
||||||
FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const sampleblockBuffer[], void *client_data) {
|
FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const sampleblockBuffer[], void *client_data) {
|
||||||
FlacDecoder *flacDecoder = (__bridge FlacDecoder *)client_data;
|
FlacDecoder *flacDecoder = (__bridge FlacDecoder *)client_data;
|
||||||
|
|
||||||
|
uint32_t channels = frame->header.channels;
|
||||||
|
uint32_t bitsPerSample = frame->header.bits_per_sample;
|
||||||
|
uint32_t frequency = frame->header.sample_rate;
|
||||||
|
|
||||||
|
if(channels != flacDecoder->channels ||
|
||||||
|
bitsPerSample != flacDecoder->bitsPerSample ||
|
||||||
|
frequency != flacDecoder->frequency) {
|
||||||
|
if(channels != flacDecoder->channels) {
|
||||||
|
flacDecoder->channelConfig = 0;
|
||||||
|
}
|
||||||
|
flacDecoder->channels = channels;
|
||||||
|
flacDecoder->bitsPerSample = bitsPerSample;
|
||||||
|
flacDecoder->frequency = frequency;
|
||||||
|
[flacDecoder willChangeValueForKey:@"properties"];
|
||||||
|
[flacDecoder didChangeValueForKey:@"properties"];
|
||||||
|
}
|
||||||
|
|
||||||
void *blockBuffer = [flacDecoder blockBuffer];
|
void *blockBuffer = [flacDecoder blockBuffer];
|
||||||
|
|
||||||
int8_t *alias8;
|
int8_t *alias8;
|
||||||
|
@ -185,6 +202,7 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta
|
||||||
|
|
||||||
if(!flacDecoder->hasStreamInfo) {
|
if(!flacDecoder->hasStreamInfo) {
|
||||||
flacDecoder->channels = metadata->data.stream_info.channels;
|
flacDecoder->channels = metadata->data.stream_info.channels;
|
||||||
|
flacDecoder->channelConfig = 0;
|
||||||
flacDecoder->frequency = metadata->data.stream_info.sample_rate;
|
flacDecoder->frequency = metadata->data.stream_info.sample_rate;
|
||||||
flacDecoder->bitsPerSample = metadata->data.stream_info.bits_per_sample;
|
flacDecoder->bitsPerSample = metadata->data.stream_info.bits_per_sample;
|
||||||
|
|
||||||
|
@ -243,9 +261,12 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
|
||||||
|
|
||||||
- (int)readAudio:(void *)buffer frames:(UInt32)frames {
|
- (int)readAudio:(void *)buffer frames:(UInt32)frames {
|
||||||
int framesRead = 0;
|
int framesRead = 0;
|
||||||
int bytesPerFrame = ((bitsPerSample + 7) / 8) * channels;
|
|
||||||
while(framesRead < frames) {
|
while(framesRead < frames) {
|
||||||
if(blockBufferFrames == 0) {
|
if(blockBufferFrames == 0) {
|
||||||
|
if(framesRead) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if(FLAC__stream_decoder_get_state(decoder) == FLAC__STREAM_DECODER_END_OF_STREAM) {
|
if(FLAC__stream_decoder_get_state(decoder) == FLAC__STREAM_DECODER_END_OF_STREAM) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -253,6 +274,8 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
|
||||||
FLAC__stream_decoder_process_single(decoder);
|
FLAC__stream_decoder_process_single(decoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int bytesPerFrame = ((bitsPerSample + 7) / 8) * channels;
|
||||||
|
|
||||||
int framesToRead = blockBufferFrames;
|
int framesToRead = blockBufferFrames;
|
||||||
if(blockBufferFrames > frames) {
|
if(blockBufferFrames > frames) {
|
||||||
framesToRead = frames;
|
framesToRead = frames;
|
||||||
|
@ -333,7 +356,7 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
|
||||||
- (NSDictionary *)properties {
|
- (NSDictionary *)properties {
|
||||||
return [NSDictionary dictionaryWithObjectsAndKeys:
|
return [NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithInt:channels], @"channels",
|
[NSNumber numberWithInt:channels], @"channels",
|
||||||
[NSNumber numberWithInt:channelConfig], @"channelConfig",
|
[NSNumber numberWithUnsignedInt:channelConfig], @"channelConfig",
|
||||||
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
||||||
[NSNumber numberWithFloat:frequency], @"sampleRate",
|
[NSNumber numberWithFloat:frequency], @"sampleRate",
|
||||||
[NSNumber numberWithDouble:totalFrames], @"totalFrames",
|
[NSNumber numberWithDouble:totalFrames], @"totalFrames",
|
||||||
|
|
|
@ -283,7 +283,7 @@ int32_t WriteBytesProc(void *ds, void *data, int32_t bcount) {
|
||||||
- (NSDictionary *)properties {
|
- (NSDictionary *)properties {
|
||||||
return [NSDictionary dictionaryWithObjectsAndKeys:
|
return [NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithInt:channels], @"channels",
|
[NSNumber numberWithInt:channels], @"channels",
|
||||||
[NSNumber numberWithInt:channelConfig], @"channelConfig",
|
[NSNumber numberWithUnsignedInt:channelConfig], @"channelConfig",
|
||||||
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
[NSNumber numberWithInt:bitsPerSample], @"bitsPerSample",
|
||||||
[NSNumber numberWithInt:bitrate], @"bitrate",
|
[NSNumber numberWithInt:bitrate], @"bitrate",
|
||||||
[NSNumber numberWithFloat:frequency], @"sampleRate",
|
[NSNumber numberWithFloat:frequency], @"sampleRate",
|
||||||
|
|
Loading…
Reference in New Issue