628 lines
16 KiB
Objective-C
628 lines
16 KiB
Objective-C
//
|
|
// MADFile.m
|
|
// Cog
|
|
//
|
|
// Created by Vincent Spader on 6/17/06.
|
|
// Copyright 2006 Vincent Spader. All rights reserved.
|
|
//
|
|
|
|
#import "MADDecoder.h"
|
|
|
|
@implementation MADDecoder
|
|
|
|
#define LAME_HEADER_SIZE ((8 * 5) + 4 + 4 + 8 + 32 + 16 + 16 + 4 + 4 + 8 + 12 + 12 + 8 + 8 + 2 + 3 + 11 + 32 + 32 + 32)
|
|
|
|
// From vbrheadersdk:
|
|
// ========================================
|
|
// A Xing header may be present in the ancillary
|
|
// data field of the first frame of an mp3 bitstream
|
|
// The Xing header (optionally) contains
|
|
// frames total number of audio frames in the bitstream
|
|
// bytes total number of bytes in the bitstream
|
|
// toc table of contents
|
|
|
|
// toc (table of contents) gives seek points
|
|
// for random access
|
|
// the ith entry determines the seek point for
|
|
// i-percent duration
|
|
// seek point in bytes = (toc[i]/256.0) * total_bitstream_bytes
|
|
// e.g. half duration seek point = (toc[50]/256.0) * total_bitstream_bytes
|
|
|
|
#define FRAMES_FLAG 0x0001
|
|
#define BYTES_FLAG 0x0002
|
|
#define TOC_FLAG 0x0004
|
|
#define VBR_SCALE_FLAG 0x0008
|
|
|
|
|
|
//Scan file quickly
|
|
- (BOOL)scanFile
|
|
{
|
|
struct mad_stream stream;
|
|
struct mad_frame frame;
|
|
|
|
int framesDecoded = 0;
|
|
int bytesToRead, bytesRemaining;
|
|
int samplesPerMPEGFrame = 0;
|
|
|
|
int id3_length = 0;
|
|
|
|
mad_stream_init (&stream);
|
|
mad_frame_init (&frame);
|
|
|
|
[_source seek:0 whence:SEEK_END];
|
|
_fileSize = [_source tell];
|
|
[_source seek:0 whence:SEEK_SET];
|
|
|
|
for (;;) {
|
|
if(NULL == stream.buffer || MAD_ERROR_BUFLEN == stream.error) {
|
|
if(stream.next_frame) {
|
|
bytesRemaining = stream.bufend - stream.next_frame;
|
|
|
|
memmove(_inputBuffer, stream.next_frame, bytesRemaining);
|
|
|
|
bytesToRead = INPUT_BUFFER_SIZE - bytesRemaining;
|
|
}
|
|
else {
|
|
bytesToRead = INPUT_BUFFER_SIZE,
|
|
bytesRemaining = 0;
|
|
}
|
|
|
|
// Read raw bytes from the MP3 file
|
|
int bytesRead = [_source read:_inputBuffer + bytesRemaining amount: bytesToRead];
|
|
|
|
if (bytesRead == 0)
|
|
{
|
|
memset(_inputBuffer + bytesRemaining + bytesRead, 0, MAD_BUFFER_GUARD);
|
|
bytesRead += MAD_BUFFER_GUARD;
|
|
inputEOF = YES;
|
|
}
|
|
|
|
mad_stream_buffer(&stream, _inputBuffer, bytesRead + bytesRemaining);
|
|
stream.error = MAD_ERROR_NONE;
|
|
}
|
|
|
|
if (mad_frame_decode(&frame, &stream) == -1)
|
|
{
|
|
if (MAD_RECOVERABLE(stream.error))
|
|
{
|
|
// Prevent ID3 tags from reporting recoverable frame errors
|
|
const uint8_t *buffer = stream.this_frame;
|
|
unsigned buflen = stream.bufend - stream.this_frame;
|
|
|
|
if(10 <= buflen && 0x49 == buffer[0] && 0x44 == buffer[1] && 0x33 == buffer[2]) {
|
|
id3_length = (((buffer[6] & 0x7F) << (3 * 7)) | ((buffer[7] & 0x7F) << (2 * 7)) |
|
|
((buffer[8] & 0x7F) << (1 * 7)) | ((buffer[9] & 0x7F) << (0 * 7)));
|
|
|
|
// Add 10 bytes for ID3 header
|
|
id3_length += 10;
|
|
|
|
mad_stream_skip(&stream, id3_length);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
else if (stream.error == MAD_ERROR_BUFLEN && inputEOF)
|
|
{
|
|
break;
|
|
}
|
|
else if (stream.error == MAD_ERROR_BUFLEN)
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
//NSLog(@"Unrecoverable error: %s", mad_stream_errorstr(&stream));
|
|
break;
|
|
}
|
|
}
|
|
framesDecoded++;
|
|
|
|
if (framesDecoded == 1)
|
|
{
|
|
sampleRate = frame.header.samplerate;
|
|
channels = MAD_NCHANNELS(&frame.header);
|
|
|
|
if(MAD_FLAG_LSF_EXT & frame.header.flags || MAD_FLAG_MPEG_2_5_EXT & frame.header.flags) {
|
|
switch(frame.header.layer) {
|
|
case MAD_LAYER_I: samplesPerMPEGFrame = 384; break;
|
|
case MAD_LAYER_II: samplesPerMPEGFrame = 1152; break;
|
|
case MAD_LAYER_III: samplesPerMPEGFrame = 576; break;
|
|
}
|
|
}
|
|
else {
|
|
switch(frame.header.layer) {
|
|
case MAD_LAYER_I: samplesPerMPEGFrame = 384; break;
|
|
case MAD_LAYER_II: samplesPerMPEGFrame = 1152; break;
|
|
case MAD_LAYER_III: samplesPerMPEGFrame = 1152; break;
|
|
}
|
|
}
|
|
|
|
unsigned ancillaryBitsRemaining = stream.anc_bitlen;
|
|
|
|
if(32 > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
uint32_t magic = mad_bit_read(&stream.anc_ptr, 32);
|
|
ancillaryBitsRemaining -= 32;
|
|
|
|
if('Xing' == magic || 'Info' == magic) {
|
|
unsigned i;
|
|
uint32_t flags = 0, frames = 0, bytes = 0, vbrScale = 0;
|
|
|
|
if(32 > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
flags = mad_bit_read(&stream.anc_ptr, 32);
|
|
ancillaryBitsRemaining -= 32;
|
|
|
|
// 4 byte value containing total frames
|
|
if(FRAMES_FLAG & flags) {
|
|
if(32 > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
frames = mad_bit_read(&stream.anc_ptr, 32);
|
|
ancillaryBitsRemaining -= 32;
|
|
|
|
// Determine number of samples, discounting encoder delay and padding
|
|
// Our concept of a frame is the same as CoreAudio's- one sample across all channels
|
|
totalFrames = frames * samplesPerMPEGFrame;
|
|
//NSLog(@"TOTAL READ FROM XING");
|
|
}
|
|
|
|
// 4 byte value containing total bytes
|
|
if(BYTES_FLAG & flags) {
|
|
if(32 > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
bytes = mad_bit_read(&stream.anc_ptr, 32);
|
|
ancillaryBitsRemaining -= 32;
|
|
}
|
|
|
|
// 100 bytes containing TOC information
|
|
if(TOC_FLAG & flags) {
|
|
if(8 * 100 > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
for(i = 0; i < 100; ++i)
|
|
/*_xingTOC[i] = */ mad_bit_read(&stream.anc_ptr, 8);
|
|
|
|
ancillaryBitsRemaining -= (8* 100);
|
|
}
|
|
|
|
// 4 byte value indicating encoded vbr scale
|
|
if(VBR_SCALE_FLAG & flags) {
|
|
if(32 > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
vbrScale = mad_bit_read(&stream.anc_ptr, 32);
|
|
ancillaryBitsRemaining -= 32;
|
|
}
|
|
|
|
framesDecoded = frames;
|
|
|
|
_foundXingHeader = YES;
|
|
|
|
// Loook for the LAME header next
|
|
// http://gabriel.mp3-tech.org/mp3infotag.html
|
|
if(32 > ancillaryBitsRemaining)
|
|
continue;
|
|
magic = mad_bit_read(&stream.anc_ptr, 32);
|
|
|
|
ancillaryBitsRemaining -= 32;
|
|
|
|
if('LAME' == magic) {
|
|
|
|
if(LAME_HEADER_SIZE > ancillaryBitsRemaining)
|
|
continue;
|
|
|
|
/*unsigned char versionString [5 + 1];
|
|
memset(versionString, 0, 6);*/
|
|
|
|
for(i = 0; i < 5; ++i)
|
|
/*versionString[i] =*/ mad_bit_read(&stream.anc_ptr, 8);
|
|
|
|
/*uint8_t infoTagRevision =*/ mad_bit_read(&stream.anc_ptr, 4);
|
|
/*uint8_t vbrMethod =*/ mad_bit_read(&stream.anc_ptr, 4);
|
|
|
|
/*uint8_t lowpassFilterValue =*/ mad_bit_read(&stream.anc_ptr, 8);
|
|
|
|
/*float peakSignalAmplitude =*/ mad_bit_read(&stream.anc_ptr, 32);
|
|
/*uint16_t radioReplayGain =*/ mad_bit_read(&stream.anc_ptr, 16);
|
|
/*uint16_t audiophileReplayGain =*/ mad_bit_read(&stream.anc_ptr, 16);
|
|
|
|
/*uint8_t encodingFlags =*/ mad_bit_read(&stream.anc_ptr, 4);
|
|
/*uint8_t athType =*/ mad_bit_read(&stream.anc_ptr, 4);
|
|
|
|
/*uint8_t lameBitrate =*/ mad_bit_read(&stream.anc_ptr, 8);
|
|
|
|
_startPadding = mad_bit_read(&stream.anc_ptr, 12);
|
|
_endPadding = mad_bit_read(&stream.anc_ptr, 12);
|
|
|
|
_startPadding += 528 + 1; //MDCT/filterbank delay
|
|
_endPadding -= 528 + 1;
|
|
|
|
/*uint8_t misc =*/ mad_bit_read(&stream.anc_ptr, 8);
|
|
|
|
uint8_t mp3Gain = mad_bit_read(&stream.anc_ptr, 8);
|
|
NSLog(@"Gain: %i", mp3Gain);
|
|
|
|
/*uint8_t unused =*/mad_bit_read(&stream.anc_ptr, 2);
|
|
/*uint8_t surroundInfo =*/ mad_bit_read(&stream.anc_ptr, 3);
|
|
/*uint16_t presetInfo =*/ mad_bit_read(&stream.anc_ptr, 11);
|
|
|
|
/*uint32_t musicGain =*/ mad_bit_read(&stream.anc_ptr, 32);
|
|
|
|
/*uint32_t musicCRC =*/ mad_bit_read(&stream.anc_ptr, 32);
|
|
|
|
/*uint32_t tagCRC =*/ mad_bit_read(&stream.anc_ptr, 32);
|
|
|
|
ancillaryBitsRemaining -= LAME_HEADER_SIZE;
|
|
|
|
_foundLAMEHeader = YES;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
totalFrames = (double)frame.header.samplerate * ((_fileSize - id3_length) / (frame.header.bitrate / 8.0));
|
|
//NSLog(@"Guestimating total samples");
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
bitrate = ((double)((_fileSize - id3_length)*8)/1000.0) * (sampleRate/(double)totalFrames);
|
|
|
|
mad_frame_finish (&frame);
|
|
mad_stream_finish (&stream);
|
|
|
|
bitsPerSample = 16;
|
|
|
|
bytesPerFrame = (bitsPerSample/8) * channels;
|
|
|
|
[_source seek:0 whence:SEEK_SET];
|
|
inputEOF = NO;
|
|
|
|
NSLog(@"Mad properties: %@", [self properties]);
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL)open:(id<CogSource>)source
|
|
{
|
|
[source retain];
|
|
[_source release];
|
|
_source = source;
|
|
|
|
/* First the structures used by libmad must be initialized. */
|
|
mad_stream_init(&_stream);
|
|
mad_frame_init(&_frame);
|
|
mad_synth_init(&_synth);
|
|
|
|
_firstFrame = YES;
|
|
//NSLog(@"OPEN: %i", _firstFrame);
|
|
|
|
inputEOF = NO;
|
|
|
|
if (![_source seekable])
|
|
{
|
|
//Decode the first frame to get the channels, samplerate, etc.
|
|
int r;
|
|
do {
|
|
r = [self decodeMPEGFrame];
|
|
NSLog(@"Decoding first frame: %i", r);
|
|
} while (r == 0);
|
|
|
|
return (r == -1 ? NO : YES);
|
|
}
|
|
|
|
return [self scanFile];
|
|
}
|
|
|
|
|
|
// Clipping and rounding code from madplay(audio.c):
|
|
/*
|
|
* madplay - MPEG audio decoder and player
|
|
* Copyright (C) 2000-2004 Robert Leslie
|
|
*/
|
|
static int32_t
|
|
audio_linear_round(unsigned int bits,
|
|
mad_fixed_t sample)
|
|
{
|
|
enum {
|
|
MIN = -MAD_F_ONE,
|
|
MAX = MAD_F_ONE - 1
|
|
};
|
|
|
|
/* round */
|
|
sample += (1L << (MAD_F_FRACBITS - bits));
|
|
|
|
/* clip */
|
|
if(MAX < sample)
|
|
sample = MAX;
|
|
else if(MIN > sample)
|
|
sample = MIN;
|
|
|
|
/* quantize and scale */
|
|
return sample >> (MAD_F_FRACBITS + 1 - bits);
|
|
}
|
|
// End madplay code
|
|
|
|
|
|
- (void)writeOutput
|
|
{
|
|
unsigned int startingSample = 0;
|
|
unsigned int sampleCount = _synth.pcm.length;
|
|
|
|
// NSLog(@"Position: %li/%li", _framesDecoded, totalFrames);
|
|
// NSLog(@"<%i, %i>", _startPadding, _endPadding);
|
|
|
|
// NSLog(@"Counts: %i, %i", startingSample, sampleCount);
|
|
if (_foundLAMEHeader) {
|
|
// We are at the beginning and need to skip frames
|
|
if (_startPadding > _framesDecoded) {
|
|
// NSLog(@"Skipping start.");
|
|
startingSample = _startPadding - _framesDecoded;
|
|
}
|
|
|
|
// Past the end of the file.
|
|
if (totalFrames - _endPadding <= _framesDecoded) {
|
|
// NSLog(@"End of file. Not writing.");
|
|
return;
|
|
}
|
|
|
|
// We haven't even gotten to the start yet
|
|
if (startingSample > sampleCount) {
|
|
// NSLog(@"Skipping entire sample");
|
|
_framesDecoded += _synth.pcm.length;
|
|
return;
|
|
}
|
|
|
|
// We are at the end of the file and need to read the last few frames
|
|
if (_framesDecoded + (sampleCount - startingSample) > totalFrames - _endPadding) {
|
|
// NSLog(@"End of file. %li", totalFrames - _endPadding - _framesDecoded);
|
|
sampleCount = totalFrames - _endPadding - _framesDecoded + startingSample;
|
|
}
|
|
|
|
}
|
|
|
|
//NSLog(@"Revised: %i, %i", startingSample, sampleCount);
|
|
|
|
_framesDecoded += _synth.pcm.length;
|
|
|
|
if (_outputFrames > 0) {
|
|
NSLog(@"LOSING FRAMES!");
|
|
}
|
|
_outputFrames = (sampleCount - startingSample);
|
|
|
|
if (_outputBuffer)
|
|
free(_outputBuffer);
|
|
|
|
_outputBuffer = (unsigned char *) malloc (_outputFrames * bytesPerFrame * sizeof (char));
|
|
|
|
unsigned int i, j;
|
|
unsigned int stride = channels * 2;
|
|
for (j = 0; j < channels; j++)
|
|
{
|
|
/* output sample(s) in 16-bit signed little-endian PCM */
|
|
mad_fixed_t const *channel = _synth.pcm.samples[j];
|
|
unsigned char *outputPtr = _outputBuffer + (j * 2);
|
|
|
|
for (i = startingSample; i < sampleCount; i++)
|
|
{
|
|
signed short sample = audio_linear_round(bitsPerSample, channel[i]);
|
|
|
|
outputPtr[0] = sample>>8;
|
|
outputPtr[1] = sample & 0xff;
|
|
outputPtr += stride;
|
|
}
|
|
}
|
|
|
|
// Output to a file
|
|
// FILE *f = fopen("data.raw", "a");
|
|
// fwrite(_outputBuffer, channels * 2, _outputFrames, f);
|
|
// fclose(f);
|
|
|
|
}
|
|
|
|
- (int)decodeMPEGFrame
|
|
{
|
|
if (_stream.buffer == NULL || _stream.error == MAD_ERROR_BUFLEN)
|
|
{
|
|
int inputToRead;
|
|
int inputRemaining;
|
|
|
|
if (_stream.next_frame != NULL)
|
|
{
|
|
inputRemaining = _stream.bufend - _stream.next_frame;
|
|
|
|
memmove(_inputBuffer, _stream.next_frame, inputRemaining);
|
|
|
|
inputToRead = INPUT_BUFFER_SIZE - inputRemaining;
|
|
}
|
|
else
|
|
{
|
|
inputToRead = INPUT_BUFFER_SIZE;
|
|
inputRemaining = 0;
|
|
}
|
|
|
|
int inputRead = [_source read:_inputBuffer + inputRemaining amount:INPUT_BUFFER_SIZE - inputRemaining];
|
|
if (inputRead == 0)
|
|
{
|
|
memset(_inputBuffer + inputRemaining + inputRead, 0, MAD_BUFFER_GUARD);
|
|
inputRead += MAD_BUFFER_GUARD;
|
|
inputEOF = YES;
|
|
}
|
|
|
|
mad_stream_buffer(&_stream, _inputBuffer, inputRead + inputRemaining);
|
|
_stream.error = MAD_ERROR_NONE;
|
|
//NSLog(@"Read stream.");
|
|
}
|
|
|
|
if (mad_frame_decode(&_frame, &_stream) == -1) {
|
|
if (MAD_RECOVERABLE (_stream.error))
|
|
{
|
|
const uint8_t *buffer = _stream.this_frame;
|
|
unsigned buflen = _stream.bufend - _stream.this_frame;
|
|
uint32_t id3_length = 0;
|
|
|
|
//No longer need ID3Tag framework
|
|
if(10 <= buflen && 0x49 == buffer[0] && 0x44 == buffer[1] && 0x33 == buffer[2]) {
|
|
id3_length = (((buffer[6] & 0x7F) << (3 * 7)) | ((buffer[7] & 0x7F) << (2 * 7)) |
|
|
((buffer[8] & 0x7F) << (1 * 7)) | ((buffer[9] & 0x7F) << (0 * 7)));
|
|
|
|
// Add 10 bytes for ID3 header
|
|
id3_length += 10;
|
|
|
|
mad_stream_skip(&_stream, id3_length);
|
|
}
|
|
|
|
NSLog(@"recoverable error");
|
|
return 0;
|
|
}
|
|
else if (MAD_ERROR_BUFLEN == _stream.error && inputEOF)
|
|
{
|
|
NSLog(@"EOF");
|
|
return -1;
|
|
}
|
|
else if (MAD_ERROR_BUFLEN == _stream.error)
|
|
{
|
|
//NSLog(@"Bufferlen");
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
//NSLog(@"Unrecoverable stream error: %s", mad_stream_errorstr(&_stream));
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
//NSLog(@"Decoded buffer.");
|
|
mad_synth_frame (&_synth, &_frame);
|
|
//NSLog(@"first frame: %i", _firstFrame);
|
|
if (_firstFrame)
|
|
{
|
|
_firstFrame = NO;
|
|
|
|
if (![_source seekable]) {
|
|
sampleRate = _frame.header.samplerate;
|
|
channels = MAD_NCHANNELS(&_frame.header);
|
|
bitsPerSample = 16;
|
|
bytesPerFrame = (bitsPerSample/8) * channels;
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
[self didChangeValueForKey:@"properties"];
|
|
}
|
|
//NSLog(@"FIRST FRAME!!! %i %i", _foundXingHeader, _foundLAMEHeader);
|
|
if (_foundXingHeader) {
|
|
//NSLog(@"Skipping xing header.");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
- (int)readAudio:(void *)buffer frames:(UInt32)frames
|
|
{
|
|
int framesRead = 0;
|
|
|
|
for (;;)
|
|
{
|
|
int framesRemaining = frames - framesRead;
|
|
int framesToCopy = (_outputFrames > framesRemaining ? framesRemaining : _outputFrames);
|
|
|
|
if (framesToCopy) {
|
|
memcpy(buffer + (framesRead * bytesPerFrame), _outputBuffer, framesToCopy*bytesPerFrame);
|
|
framesRead += framesToCopy;
|
|
|
|
if (framesToCopy != _outputFrames) {
|
|
memmove(_outputBuffer, _outputBuffer + (framesToCopy*bytesPerFrame), (_outputFrames - framesToCopy)*bytesPerFrame);
|
|
}
|
|
|
|
_outputFrames -= framesToCopy;
|
|
}
|
|
|
|
if (framesRead == frames)
|
|
break;
|
|
|
|
int r = [self decodeMPEGFrame];
|
|
//NSLog(@"Decoding frame: %i", r);
|
|
if (r == 0) //Recoverable error.
|
|
continue;
|
|
else if (r == -1) //Unrecoverable error
|
|
break;
|
|
|
|
[self writeOutput];
|
|
//NSLog(@"Wrote output");
|
|
}
|
|
|
|
//NSLog(@"Read: %i/%i", bytesRead, size);
|
|
return framesRead;
|
|
}
|
|
|
|
- (void)close
|
|
{
|
|
if (_source)
|
|
{
|
|
[_source close];
|
|
[_source release];
|
|
_source = nil;
|
|
}
|
|
|
|
if (_outputBuffer)
|
|
{
|
|
free(_outputBuffer);
|
|
_outputBuffer = NULL;
|
|
}
|
|
|
|
mad_synth_finish(&_synth);
|
|
mad_frame_finish(&_frame);
|
|
mad_stream_finish(&_stream);
|
|
}
|
|
|
|
- (long)seek:(long)frame
|
|
{
|
|
if (frame > totalFrames)
|
|
frame = totalFrames;
|
|
|
|
unsigned long new_position = ((double)frame / totalFrames) * _fileSize;
|
|
[_source seek:new_position whence:SEEK_SET];
|
|
|
|
mad_stream_buffer(&_stream, NULL, 0);
|
|
|
|
//Gapless busted after seek. Mp3 just doesn't have sample-accurate seeking. Maybe xing toc?
|
|
_framesDecoded = frame;
|
|
|
|
return frame;
|
|
}
|
|
|
|
- (NSDictionary *)properties
|
|
{
|
|
return [NSDictionary dictionaryWithObjectsAndKeys:
|
|
[NSNumber numberWithInt:channels],@"channels",
|
|
[NSNumber numberWithInt:bitsPerSample],@"bitsPerSample",
|
|
[NSNumber numberWithFloat:sampleRate],@"sampleRate",
|
|
[NSNumber numberWithInt:bitrate],@"bitrate",
|
|
[NSNumber numberWithLong:totalFrames - (_startPadding + _endPadding)],@"totalFrames",
|
|
[NSNumber numberWithBool:[_source seekable]], @"seekable",
|
|
@"big", @"endian",
|
|
nil];
|
|
}
|
|
|
|
|
|
+ (NSArray *)fileTypes
|
|
{
|
|
return [NSArray arrayWithObjects:@"mp3",nil];
|
|
}
|
|
|
|
+ (NSArray *)mimeTypes
|
|
{
|
|
return [NSArray arrayWithObjects:@"audio/mpeg", @"audio/x-mp3", nil];
|
|
}
|
|
|
|
@end
|
|
|