2022-02-25 01:48:10 +00:00
|
|
|
//
|
|
|
|
// MADFile.m
|
|
|
|
// Cog
|
|
|
|
//
|
|
|
|
// Created by Vincent Spader on 6/17/06.
|
|
|
|
// Copyright 2006 Vincent Spader. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#import "MADDecoder.h"
|
|
|
|
|
|
|
|
#import "HTTPSource.h"
|
|
|
|
|
|
|
|
#import "Logging.h"
|
|
|
|
|
|
|
|
#import "id3tag.h"
|
|
|
|
|
|
|
|
#import <Accelerate/Accelerate.h>
|
|
|
|
|
2022-06-20 02:40:40 +00:00
|
|
|
#import "CVbriHeader.h"
|
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
@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
|
|
|
|
- (void)bufferRefill:(struct mad_stream *)stream {
|
|
|
|
long bytesToRead, bytesRemaining;
|
|
|
|
|
|
|
|
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
|
|
|
|
long 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)scanFile {
|
|
|
|
struct mad_stream stream;
|
|
|
|
struct mad_frame frame;
|
|
|
|
|
|
|
|
long framesDecoded = 0;
|
|
|
|
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(;;) {
|
|
|
|
[self bufferRefill:&stream];
|
|
|
|
|
|
|
|
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 long 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)));
|
|
|
|
|
|
|
|
_foundID3v2 = YES;
|
|
|
|
|
|
|
|
// Add 10 bytes for ID3 header
|
|
|
|
id3_length += 10;
|
|
|
|
|
|
|
|
void *tagBuffer = malloc(id3_length);
|
|
|
|
if(!tagBuffer) return NO;
|
|
|
|
|
|
|
|
memcpy(tagBuffer, &buffer[0], MIN(buflen, id3_length));
|
|
|
|
|
|
|
|
long bufleft = id3_length - buflen;
|
|
|
|
long tagRead = MIN(buflen, id3_length);
|
|
|
|
|
|
|
|
while(bufleft > 0) {
|
|
|
|
stream.error = MAD_ERROR_BUFLEN;
|
|
|
|
stream.next_frame = NULL;
|
|
|
|
[self bufferRefill:&stream];
|
|
|
|
buffer = stream.this_frame;
|
|
|
|
buflen = stream.bufend - stream.this_frame;
|
|
|
|
memcpy(tagBuffer + tagRead, buffer, MIN(buflen, bufleft));
|
|
|
|
tagRead += MIN(buflen, bufleft);
|
|
|
|
bufleft -= buflen;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(bufleft < 0) {
|
|
|
|
mad_stream_skip(&stream, buflen + bufleft);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct id3_tag *tag = id3_tag_parse(tagBuffer, id3_length);
|
|
|
|
|
|
|
|
if(tag) {
|
|
|
|
for(size_t i = 0; i < tag->nframes; ++i) {
|
|
|
|
struct id3_frame *frame = tag->frames[i];
|
|
|
|
if(!strcmp(frame->id, "COMM")) {
|
|
|
|
union id3_field *field;
|
|
|
|
const id3_ucs4_t *description;
|
|
|
|
const id3_ucs4_t *value;
|
|
|
|
|
|
|
|
field = id3_frame_field(frame, 2);
|
|
|
|
description = id3_field_getstring(field);
|
|
|
|
|
|
|
|
field = id3_frame_field(frame, 3);
|
|
|
|
value = id3_field_getfullstring(field);
|
|
|
|
|
|
|
|
if(description && value) {
|
|
|
|
id3_utf8_t *description8 = id3_ucs4_utf8duplicate(description);
|
|
|
|
if(!strcmp((const char *)description8, "iTunSMPB")) {
|
|
|
|
id3_utf8_t *value8 = id3_ucs4_utf8duplicate(value);
|
|
|
|
|
|
|
|
uint32_t zero, start_pad, end_pad;
|
|
|
|
uint64_t last_eight_frames_offset;
|
|
|
|
int64_t temp_duration;
|
|
|
|
|
|
|
|
if(sscanf((const char *)value8, "%" PRIx32 " %" PRIx32 " %" PRIx32 " %" PRIx64 " %" PRIx32 " %" PRIx64, &zero, &start_pad, &end_pad, &temp_duration, &zero, &last_eight_frames_offset) == 6 &&
|
|
|
|
temp_duration >= 0 &&
|
|
|
|
start_pad <= (576 * 2 * 32) &&
|
|
|
|
end_pad <= (576 * 2 * 64) &&
|
|
|
|
(_fileSize && (last_eight_frames_offset < (_fileSize - id3_length)))) {
|
|
|
|
if(end_pad >= 528 + 1) {
|
|
|
|
_startPadding = start_pad + 528 + 1;
|
|
|
|
_endPadding = end_pad - (528 + 1);
|
2022-03-08 07:59:14 +00:00
|
|
|
// iTunes encodes the original length of the file here
|
|
|
|
totalFrames = temp_duration + _startPadding + _endPadding;
|
2022-02-25 01:48:10 +00:00
|
|
|
_foundiTunSMPB = YES;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
free(value8);
|
|
|
|
}
|
|
|
|
free(description8);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
id3_tag_delete(tag);
|
|
|
|
}
|
|
|
|
|
|
|
|
free(tagBuffer);
|
2022-06-20 01:26:18 +00:00
|
|
|
} else if(stream.error == MAD_ERROR_BADDATAPTR) {
|
|
|
|
goto framedecoded;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
} else if(stream.error == MAD_ERROR_BUFLEN && inputEOF) {
|
|
|
|
break;
|
|
|
|
} else if(stream.error == MAD_ERROR_BUFLEN) {
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
// DLog(@"Unrecoverable error: %s", mad_stream_errorstr(&stream));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-06-20 01:26:18 +00:00
|
|
|
framedecoded:
|
2022-02-25 01:48:10 +00:00
|
|
|
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;
|
|
|
|
layer = 1;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_II:
|
|
|
|
samplesPerMPEGFrame = 1152;
|
|
|
|
layer = 2;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_III:
|
|
|
|
samplesPerMPEGFrame = 576;
|
|
|
|
layer = 3;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch(frame.header.layer) {
|
|
|
|
case MAD_LAYER_I:
|
|
|
|
samplesPerMPEGFrame = 384;
|
|
|
|
layer = 1;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_II:
|
|
|
|
samplesPerMPEGFrame = 1152;
|
|
|
|
layer = 2;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_III:
|
|
|
|
samplesPerMPEGFrame = 1152;
|
|
|
|
layer = 3;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-06-21 12:33:15 +00:00
|
|
|
|
|
|
|
if(layer != 3) continue;
|
|
|
|
|
|
|
|
const size_t ancillaryBitsRemaining = (stream.next_frame - stream.this_frame) * 8;
|
|
|
|
|
|
|
|
static const int64_t xing_offtbl[2][2] = {{32, 17}, {17,9}};
|
|
|
|
|
|
|
|
const int64_t xing_offset = xing_offtbl[!!(MAD_FLAG_LSF_EXT & frame.header.flags || MAD_FLAG_MPEG_2_5_EXT & frame.header.flags)][channels == 1] + 4; // Plus MPEG header
|
|
|
|
|
|
|
|
size_t ancBitsRemainingXing = ancillaryBitsRemaining - xing_offset * 8;
|
|
|
|
|
|
|
|
if(ancBitsRemainingXing >= 32) {
|
|
|
|
const uint8_t *ptr = stream.this_frame + xing_offset;
|
|
|
|
struct mad_bitptr bitptr;
|
|
|
|
|
|
|
|
mad_bit_init(&bitptr, ptr);
|
|
|
|
uint32_t magic = (uint32_t)mad_bit_read(&bitptr, 32);
|
|
|
|
ancBitsRemainingXing -= 32;
|
|
|
|
|
|
|
|
if('Xing' == magic || 'Info' == magic) {
|
|
|
|
unsigned i;
|
|
|
|
uint32_t flags = 0, frames = 0, bytes = 0, vbrScale = 0;
|
|
|
|
|
|
|
|
if(32 > ancBitsRemainingXing)
|
|
|
|
continue;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
flags = (uint32_t)mad_bit_read(&bitptr, 32);
|
|
|
|
ancBitsRemainingXing -= 32;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
// 4 byte value containing total frames
|
|
|
|
if(FRAMES_FLAG & flags) {
|
|
|
|
if(32 > ancBitsRemainingXing)
|
|
|
|
continue;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
frames = (uint32_t)mad_bit_read(&bitptr, 32);
|
|
|
|
ancBitsRemainingXing -= 32;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
// 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;
|
|
|
|
// DLog(@"TOTAL READ FROM XING");
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
// 4 byte value containing total bytes
|
|
|
|
if(BYTES_FLAG & flags) {
|
|
|
|
if(32 > ancBitsRemainingXing)
|
|
|
|
continue;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
bytes = (uint32_t)mad_bit_read(&bitptr, 32);
|
|
|
|
ancBitsRemainingXing -= 32;
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
// 100 bytes containing TOC information
|
|
|
|
if(TOC_FLAG & flags) {
|
|
|
|
if(8 * 100 > ancBitsRemainingXing)
|
|
|
|
continue;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
for(i = 0; i < 100; ++i)
|
|
|
|
/*_xingTOC[i] = */ mad_bit_read(&bitptr, 8);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
ancBitsRemainingXing -= (8 * 100);
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
// 4 byte value indicating encoded vbr scale
|
|
|
|
if(VBR_SCALE_FLAG & flags) {
|
|
|
|
if(32 > ancBitsRemainingXing)
|
|
|
|
continue;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
vbrScale = (uint32_t)mad_bit_read(&bitptr, 32);
|
|
|
|
ancBitsRemainingXing -= 32;
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
framesDecoded = frames;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
_foundXingHeader = YES;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
// Loook for the LAME header next
|
|
|
|
// http://gabriel.mp3-tech.org/mp3infotag.html
|
|
|
|
if(32 > ancBitsRemainingXing)
|
2022-02-25 01:48:10 +00:00
|
|
|
continue;
|
2022-06-21 12:33:15 +00:00
|
|
|
magic = (uint32_t)mad_bit_read(&bitptr, 32);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
ancBitsRemainingXing -= 32;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
if('LAME' == magic || 'Lavf' == magic || 'Lavc' == magic) {
|
|
|
|
if(LAME_HEADER_SIZE > ancBitsRemainingXing)
|
|
|
|
continue;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*unsigned char versionString [5 + 1];
|
|
|
|
memset(versionString, 0, 6);*/
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
for(i = 0; i < 5; ++i)
|
|
|
|
/*versionString[i] =*/mad_bit_read(&bitptr, 8);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t infoTagRevision =*/mad_bit_read(&bitptr, 4);
|
|
|
|
/*uint8_t vbrMethod =*/mad_bit_read(&bitptr, 4);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t lowpassFilterValue =*/mad_bit_read(&bitptr, 8);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*float peakSignalAmplitude =*/mad_bit_read(&bitptr, 32);
|
|
|
|
/*uint16_t radioReplayGain =*/mad_bit_read(&bitptr, 16);
|
|
|
|
/*uint16_t audiophileReplayGain =*/mad_bit_read(&bitptr, 16);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t encodingFlags =*/mad_bit_read(&bitptr, 4);
|
|
|
|
/*uint8_t athType =*/mad_bit_read(&bitptr, 4);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t lameBitrate =*/mad_bit_read(&bitptr, 8);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
_startPadding = mad_bit_read(&bitptr, 12);
|
|
|
|
_endPadding = mad_bit_read(&bitptr, 12);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
_startPadding += 528 + 1; // MDCT/filterbank delay
|
|
|
|
_endPadding -= 528 + 1;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t misc =*/mad_bit_read(&bitptr, 8);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t mp3Gain =*/mad_bit_read(&bitptr, 8);
|
|
|
|
/*DLog(@"Gain: %i", mp3Gain);*/
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint8_t unused =*/mad_bit_read(&bitptr, 2);
|
|
|
|
/*uint8_t surroundInfo =*/mad_bit_read(&bitptr, 3);
|
|
|
|
/*uint16_t presetInfo =*/mad_bit_read(&bitptr, 11);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint32_t musicGain =*/mad_bit_read(&bitptr, 32);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint32_t musicCRC =*/mad_bit_read(&bitptr, 32);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
/*uint32_t tagCRC =*/mad_bit_read(&bitptr, 32);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
ancBitsRemainingXing -= LAME_HEADER_SIZE;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
_foundLAMEHeader = YES;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const size_t vbri_offset = 4 + 32;
|
|
|
|
|
|
|
|
size_t ancBitsRemainingVBRI = ancillaryBitsRemaining - vbri_offset * 8;
|
|
|
|
|
|
|
|
if(ancBitsRemainingVBRI >= 32) {
|
|
|
|
const uint8_t *ptr = stream.this_frame + vbri_offset;
|
|
|
|
struct mad_bitptr bitptr;
|
|
|
|
mad_bit_init(&bitptr, ptr);
|
|
|
|
|
|
|
|
uint32_t magic = (uint32_t)mad_bit_read(&bitptr, 32);
|
|
|
|
ancBitsRemainingVBRI -= 32;
|
|
|
|
|
|
|
|
if('VBRI' == magic) {
|
|
|
|
struct VbriHeader *vbri_header = 0;
|
|
|
|
if(readVbriHeader(&vbri_header, mad_bit_nextbyte(&bitptr), ancBitsRemainingVBRI / 8) == 0) {
|
|
|
|
uint32_t frames = VbriTotalFrames(vbri_header);
|
|
|
|
totalFrames = frames * samplesPerMPEGFrame;
|
|
|
|
_startPadding = 0;
|
|
|
|
_endPadding = 0;
|
|
|
|
|
|
|
|
_foundVBRIHeader = YES;
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
2022-06-21 12:33:15 +00:00
|
|
|
if(vbri_header) {
|
|
|
|
freeVbriHeader(vbri_header);
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-06-20 02:40:40 +00:00
|
|
|
} else if(_foundXingHeader || _foundiTunSMPB || _foundVBRIHeader) {
|
2022-06-12 23:09:56 +00:00
|
|
|
break;
|
|
|
|
} else if(framesDecoded > 1) {
|
2022-02-25 01:48:10 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-16 05:51:04 +00:00
|
|
|
// Don't commit division by zero on bad files
|
|
|
|
if(stream.next_frame == stream.this_frame) {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2022-06-20 02:40:40 +00:00
|
|
|
if(!_foundiTunSMPB && !_foundXingHeader && !_foundVBRIHeader) {
|
2022-06-12 23:09:56 +00:00
|
|
|
// Now do CBR estimation instead of full file scanning
|
|
|
|
size_t frameCount = (_fileSize - id3_length) / (stream.next_frame - stream.this_frame);
|
|
|
|
mad_timer_t duration = frame.header.duration;
|
|
|
|
mad_timer_multiply(&duration, frameCount);
|
2022-02-25 01:48:10 +00:00
|
|
|
totalFrames = mad_timer_count(duration, sampleRate);
|
|
|
|
}
|
|
|
|
|
|
|
|
bitrate = ((double)((_fileSize - id3_length) * 8) / 1000.0) * (sampleRate / (double)totalFrames);
|
|
|
|
|
|
|
|
mad_frame_finish(&frame);
|
|
|
|
mad_stream_finish(&stream);
|
|
|
|
|
|
|
|
[_source seek:0 whence:SEEK_SET];
|
|
|
|
inputEOF = NO;
|
|
|
|
|
|
|
|
DLog(@"Mad properties: %@", [self properties]);
|
|
|
|
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)open:(id<CogSource>)source {
|
|
|
|
_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;
|
2022-06-11 21:21:18 +00:00
|
|
|
_outputFrames = 0;
|
2022-06-12 23:09:56 +00:00
|
|
|
_startPadding = 0;
|
|
|
|
_endPadding = 0;
|
2022-02-25 01:48:10 +00:00
|
|
|
// DLog(@"OPEN: %i", _firstFrame);
|
|
|
|
|
|
|
|
inputEOF = NO;
|
|
|
|
|
|
|
|
genre = @"";
|
|
|
|
album = @"";
|
|
|
|
artist = @"";
|
|
|
|
title = @"";
|
|
|
|
|
|
|
|
if(![_source seekable]) {
|
|
|
|
// Decode the first frame to get the channels, samplerate, etc.
|
|
|
|
int r;
|
|
|
|
do {
|
|
|
|
r = [self decodeMPEGFrame];
|
|
|
|
DLog(@"Decoding first frame: %i", r);
|
|
|
|
} while(r == 0);
|
|
|
|
|
2022-07-21 10:50:39 +00:00
|
|
|
if(r != -1) {
|
|
|
|
metadataUpdateInterval = sampleRate;
|
|
|
|
metadataUpdateCount = 0;
|
|
|
|
}
|
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
return (r == -1 ? NO : YES);
|
|
|
|
}
|
|
|
|
|
|
|
|
framesToSkip = 0;
|
|
|
|
|
|
|
|
BOOL ret = [self scanFile];
|
|
|
|
|
|
|
|
if(_foundLAMEHeader || _foundiTunSMPB) {
|
|
|
|
framesToSkip = _startPadding;
|
|
|
|
}
|
|
|
|
|
2022-07-21 10:50:39 +00:00
|
|
|
metadataUpdateInterval = sampleRate;
|
|
|
|
metadataUpdateCount = 0;
|
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2022-07-10 23:24:08 +00:00
|
|
|
- (BOOL)writeOutput {
|
2022-02-25 01:48:10 +00:00
|
|
|
unsigned long startingSample = 0;
|
|
|
|
unsigned long sampleCount = _synth.pcm.length;
|
|
|
|
|
|
|
|
// DLog(@"Position: %li/%li", _framesDecoded, totalFrames);
|
|
|
|
// DLog(@"<%i, %i>", _startPadding, _endPadding);
|
|
|
|
if(framesToSkip > 0) {
|
|
|
|
startingSample = framesToSkip;
|
|
|
|
}
|
|
|
|
|
|
|
|
// DLog(@"Counts: %i, %i", startingSample, sampleCount);
|
|
|
|
if(_foundLAMEHeader || _foundiTunSMPB) {
|
|
|
|
// Past the end of the file.
|
|
|
|
if(totalFrames - _endPadding <= _framesDecoded) {
|
|
|
|
// DLog(@"End of file. Not writing.");
|
2022-07-10 23:24:08 +00:00
|
|
|
return YES;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
2022-06-10 09:44:02 +00:00
|
|
|
// Clip this for the following calculation, so this doesn't underflow
|
|
|
|
// when seeking and skipping a lot of samples
|
|
|
|
unsigned long startingSampleClipped = MIN(startingSample, sampleCount);
|
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
// We are at the end of the file and need to read the last few frames
|
2022-06-10 09:44:02 +00:00
|
|
|
if(_framesDecoded + (sampleCount - startingSampleClipped) > totalFrames - _endPadding) {
|
2022-02-25 01:48:10 +00:00
|
|
|
// DLog(@"End of file. %li", totalFrames - _endPadding - _framesDecoded);
|
|
|
|
sampleCount = totalFrames - _endPadding - _framesDecoded + startingSample;
|
|
|
|
}
|
2022-07-10 23:24:08 +00:00
|
|
|
} else {
|
|
|
|
// Past the end of the file.
|
2022-07-12 02:41:06 +00:00
|
|
|
if([_source seekable] && totalFrames <= _framesDecoded) {
|
2022-07-10 23:24:08 +00:00
|
|
|
return YES;
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// We haven't even gotten to the start yet
|
|
|
|
if(startingSample >= sampleCount) {
|
|
|
|
// DLog(@"Skipping entire sample");
|
|
|
|
_framesDecoded += sampleCount;
|
|
|
|
framesToSkip -= sampleCount;
|
2022-07-10 23:24:08 +00:00
|
|
|
return NO;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
framesToSkip = 0;
|
|
|
|
|
|
|
|
// DLog(@"Revised: %i, %i", startingSample, sampleCount);
|
|
|
|
|
2022-04-20 06:56:17 +00:00
|
|
|
_framesDecoded += sampleCount;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
|
|
|
if(_outputFrames > 0) {
|
|
|
|
DLog(@"LOSING FRAMES!");
|
|
|
|
}
|
|
|
|
_outputFrames = (sampleCount - startingSample);
|
|
|
|
|
|
|
|
if(_currentOutputFrames < _outputFrames) {
|
|
|
|
_outputBuffer = (float *)realloc(_outputBuffer, _outputFrames * channels * sizeof(float));
|
|
|
|
_currentOutputFrames = _outputFrames;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ch;
|
|
|
|
|
|
|
|
// samples [0 ... n]
|
|
|
|
for(ch = 0; ch < channels; ch++) {
|
|
|
|
vDSP_vflt32(&_synth.pcm.samples[ch][startingSample], 1, &_outputBuffer[ch], channels, _outputFrames);
|
|
|
|
}
|
2022-02-26 09:08:55 +00:00
|
|
|
float scale = (float)MAD_F_ONE;
|
|
|
|
vDSP_vsdiv(&_outputBuffer[0], 1, &scale, &_outputBuffer[0], 1, _outputFrames * channels);
|
2022-02-25 01:48:10 +00:00
|
|
|
|
|
|
|
// Output to a file
|
|
|
|
// FILE *f = fopen("data.raw", "a");
|
|
|
|
// fwrite(_outputBuffer, channels * 2, _outputFrames, f);
|
|
|
|
// fclose(f);
|
2022-07-10 23:24:08 +00:00
|
|
|
|
|
|
|
return NO;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (int)decodeMPEGFrame {
|
|
|
|
if(_stream.buffer == NULL || _stream.error == MAD_ERROR_BUFLEN) {
|
|
|
|
int inputToRead;
|
|
|
|
int inputRemaining;
|
|
|
|
|
|
|
|
if(_stream.next_frame != NULL) {
|
|
|
|
inputRemaining = (int)(_stream.bufend - _stream.next_frame);
|
|
|
|
|
|
|
|
memmove(_inputBuffer, _stream.next_frame, inputRemaining);
|
|
|
|
|
|
|
|
inputToRead = INPUT_BUFFER_SIZE - inputRemaining;
|
|
|
|
} else {
|
|
|
|
inputToRead = INPUT_BUFFER_SIZE;
|
|
|
|
inputRemaining = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
long 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;
|
|
|
|
// DLog(@"Read stream.");
|
|
|
|
}
|
|
|
|
|
2022-06-21 12:48:58 +00:00
|
|
|
BOOL skippingBadFrame = NO;
|
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
if(mad_frame_decode(&_frame, &_stream) == -1) {
|
2022-06-21 12:48:58 +00:00
|
|
|
if(_stream.error == MAD_ERROR_BADDATAPTR) {
|
|
|
|
skippingBadFrame = YES;
|
|
|
|
} else if(MAD_RECOVERABLE(_stream.error)) {
|
2022-02-25 01:48:10 +00:00
|
|
|
const uint8_t *buffer = _stream.this_frame;
|
|
|
|
unsigned long 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
DLog(@"recoverable error");
|
|
|
|
return 0;
|
|
|
|
} else if(MAD_ERROR_BUFLEN == _stream.error && inputEOF) {
|
|
|
|
DLog(@"EOF");
|
|
|
|
return -1;
|
|
|
|
} else if(MAD_ERROR_BUFLEN == _stream.error) {
|
|
|
|
// DLog(@"Bufferlen");
|
|
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
// DLog(@"Unrecoverable stream error: %s", mad_stream_errorstr(&_stream));
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-21 12:48:58 +00:00
|
|
|
if(!_firstFrame || !(_foundXingHeader && _foundVBRIHeader)) {
|
2022-04-20 06:56:17 +00:00
|
|
|
signed long frameDuration = mad_timer_count(_frame.header.duration, sampleRate);
|
|
|
|
if((framesToSkip - 1152 * 4) >= frameDuration) {
|
|
|
|
framesToSkip -= frameDuration;
|
|
|
|
_framesDecoded += frameDuration;
|
|
|
|
return 0;
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// DLog(@"Decoded buffer.");
|
2022-06-21 12:48:58 +00:00
|
|
|
if(!skippingBadFrame) {
|
|
|
|
mad_synth_frame(&_synth, &_frame);
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
// DLog(@"first frame: %i", _firstFrame);
|
|
|
|
if(_firstFrame) {
|
|
|
|
_firstFrame = NO;
|
|
|
|
|
|
|
|
if(![_source seekable]) {
|
|
|
|
sampleRate = _frame.header.samplerate;
|
|
|
|
channels = MAD_NCHANNELS(&_frame.header);
|
|
|
|
|
|
|
|
switch(_frame.header.layer) {
|
|
|
|
case MAD_LAYER_I:
|
|
|
|
layer = 1;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_II:
|
|
|
|
layer = 2;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_III:
|
|
|
|
layer = 3;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
|
|
[self didChangeValueForKey:@"properties"];
|
|
|
|
}
|
|
|
|
// DLog(@"FIRST FRAME!!! %i %i", _foundXingHeader, _foundLAMEHeader);
|
2022-06-20 02:40:40 +00:00
|
|
|
if(_foundXingHeader || _foundVBRIHeader) {
|
2022-02-25 01:48:10 +00:00
|
|
|
// DLog(@"Skipping xing header.");
|
|
|
|
return 0;
|
|
|
|
}
|
2022-06-21 12:48:58 +00:00
|
|
|
} else if(skippingBadFrame) {
|
|
|
|
return 0;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
- (BOOL)syncFormat {
|
2022-02-26 09:43:54 +00:00
|
|
|
float _sampleRate = _frame.header.samplerate;
|
|
|
|
int _channels = MAD_NCHANNELS(&_frame.header);
|
|
|
|
int _layer = 3;
|
|
|
|
|
|
|
|
switch(_frame.header.layer) {
|
|
|
|
case MAD_LAYER_I:
|
|
|
|
_layer = 1;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_II:
|
|
|
|
_layer = 2;
|
|
|
|
break;
|
|
|
|
case MAD_LAYER_III:
|
|
|
|
_layer = 3;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL changed = (_sampleRate != sampleRate ||
|
|
|
|
_channels != channels ||
|
|
|
|
_layer != layer);
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
if(changed) {
|
2022-02-26 09:43:54 +00:00
|
|
|
sampleRate = _sampleRate;
|
|
|
|
channels = _channels;
|
|
|
|
layer = _layer;
|
|
|
|
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
|
|
[self didChangeValueForKey:@"properties"];
|
|
|
|
}
|
|
|
|
|
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
- (AudioChunk *)readAudio {
|
2022-02-26 12:26:12 +00:00
|
|
|
if(!_firstFrame)
|
2022-07-10 22:14:47 +00:00
|
|
|
[self syncFormat];
|
|
|
|
|
|
|
|
id audioChunkClass = NSClassFromString(@"AudioChunk");
|
|
|
|
AudioChunk *chunk = nil;
|
2022-02-26 09:43:54 +00:00
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
for(;;) {
|
2022-07-10 22:14:47 +00:00
|
|
|
long framesToCopy = _outputFrames;
|
2022-02-25 01:48:10 +00:00
|
|
|
|
|
|
|
if(framesToCopy) {
|
2022-07-10 22:14:47 +00:00
|
|
|
chunk = [[audioChunkClass alloc] initWithProperties:[self properties]];
|
|
|
|
[chunk assignSamples:_outputBuffer frameCount:framesToCopy];
|
|
|
|
_outputFrames = 0;
|
2022-02-25 01:48:10 +00:00
|
|
|
break;
|
2022-07-10 22:14:47 +00:00
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
|
|
|
int r = [self decodeMPEGFrame];
|
|
|
|
// DLog(@"Decoding frame: %i", r);
|
|
|
|
if(r == 0) // Recoverable error.
|
|
|
|
continue;
|
|
|
|
else if(r == -1) // Unrecoverable error
|
|
|
|
break;
|
|
|
|
|
2022-07-10 23:24:08 +00:00
|
|
|
if([self writeOutput]) {
|
|
|
|
return nil;
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
// DLog(@"Wrote output");
|
2022-02-26 09:43:54 +00:00
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
[self syncFormat];
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
2022-07-21 10:50:39 +00:00
|
|
|
metadataUpdateCount += chunk ? [chunk frameCount] : 0;
|
|
|
|
if(metadataUpdateCount >= metadataUpdateInterval) {
|
|
|
|
metadataUpdateCount -= metadataUpdateInterval;
|
|
|
|
[self updateMetadata];
|
|
|
|
}
|
2022-02-25 01:48:10 +00:00
|
|
|
|
|
|
|
// DLog(@"Read: %i/%i", bytesRead, size);
|
2022-07-10 22:14:47 +00:00
|
|
|
return chunk;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)close {
|
|
|
|
if(_source) {
|
|
|
|
[_source close];
|
|
|
|
_source = nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(_outputBuffer) {
|
|
|
|
free(_outputBuffer);
|
|
|
|
_outputBuffer = NULL;
|
2022-02-26 09:38:17 +00:00
|
|
|
_currentOutputFrames = 0;
|
2022-02-25 01:48:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
mad_synth_finish(&_synth);
|
|
|
|
mad_frame_finish(&_frame);
|
|
|
|
mad_stream_finish(&_stream);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (long)seek:(long)frame {
|
MAD Input: Fixed gapless handling on file start
This error was caused by the necessary fix of the previous commit, only
it caused something completely different. Due to the fact that MP3 is
included in the list of formats supported for embedded CUE Sheets, the
open stage performs a seek to the file start after opening the file,
even if there is no sheet embedded. And the resulting seek was supposed
to be a null operation, since the file was already at the start. But, as
a result, this reset the start skip counter to zero, and because the
offset wasn't backwards, but to the same position, it didn't reset the
skip counter to the start of track delay. So, as a result, start of
track delay wasn't being removed, introducing a gap. Now, this change
bypasses the seek function altogether if seeking would do nothing from
the current playback position. Whew.
Fixes #250 and MP3 gaplessness in general, surprised I didn't notice
this sooner myself, but I guess I didn't bother to verify whether my
change would break anything. Whoops.
Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-04-20 05:57:21 +00:00
|
|
|
if(frame == _framesDecoded) {
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
2022-02-25 01:48:10 +00:00
|
|
|
if(frame > totalFrames)
|
|
|
|
frame = totalFrames;
|
|
|
|
|
|
|
|
framesToSkip = 0;
|
|
|
|
|
2022-04-20 06:19:52 +00:00
|
|
|
if(_foundLAMEHeader || _foundiTunSMPB) {
|
|
|
|
if(_framesDecoded < _startPadding) {
|
|
|
|
framesToSkip = _startPadding;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-02 02:34:54 +00:00
|
|
|
if(frame < _framesDecoded) {
|
2022-02-25 01:48:10 +00:00
|
|
|
_framesDecoded = 0;
|
|
|
|
_firstFrame = YES;
|
|
|
|
if(_foundLAMEHeader || _foundiTunSMPB)
|
|
|
|
framesToSkip = _startPadding;
|
|
|
|
[_source seek:0 whence:SEEK_SET];
|
|
|
|
|
|
|
|
mad_stream_buffer(&_stream, NULL, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
framesToSkip += frame - _framesDecoded;
|
|
|
|
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)updateMetadata {
|
|
|
|
NSString *_artist = artist;
|
|
|
|
NSString *_album = album;
|
|
|
|
NSString *_title = title;
|
|
|
|
NSString *_genre = genre;
|
|
|
|
|
|
|
|
Class sourceClass = [_source class];
|
|
|
|
if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) {
|
|
|
|
HTTPSource *httpSource = (HTTPSource *)_source;
|
|
|
|
if([httpSource hasMetadata]) {
|
|
|
|
NSDictionary *metadata = [httpSource metadata];
|
|
|
|
_genre = [metadata valueForKey:@"genre"];
|
|
|
|
_album = [metadata valueForKey:@"album"];
|
|
|
|
_artist = [metadata valueForKey:@"artist"];
|
|
|
|
_title = [metadata valueForKey:@"title"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(![_artist isEqual:artist] ||
|
|
|
|
![_album isEqual:album] ||
|
|
|
|
![_title isEqual:title] ||
|
|
|
|
![_genre isEqual:genre]) {
|
|
|
|
artist = _artist;
|
|
|
|
album = _album;
|
|
|
|
title = _title;
|
|
|
|
genre = _genre;
|
|
|
|
if(![_source seekable]) {
|
|
|
|
[self willChangeValueForKey:@"metadata"];
|
|
|
|
[self didChangeValueForKey:@"metadata"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSDictionary *)properties {
|
2022-06-10 09:51:19 +00:00
|
|
|
if(layer < 1 || layer > 3) return nil;
|
2022-02-25 01:48:10 +00:00
|
|
|
const NSString *layers[3] = { @"MP1", @"MP2", @"MP3" };
|
2022-06-17 13:39:02 +00:00
|
|
|
return @{ @"channels": @(channels),
|
|
|
|
@"bitsPerSample": @(32),
|
|
|
|
@"sampleRate": @(sampleRate),
|
|
|
|
@"floatingPoint": @(YES),
|
|
|
|
@"bitrate": @(bitrate),
|
|
|
|
@"totalFrames": @(totalFrames - (_startPadding + _endPadding)),
|
|
|
|
@"seekable": @([_source seekable]),
|
2022-02-25 01:48:10 +00:00
|
|
|
@"codec": layers[layer - 1],
|
|
|
|
@"endian": @"host",
|
|
|
|
@"encoding": @"lossy" };
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSDictionary *)metadata {
|
|
|
|
return @{ @"artist": artist, @"album": album, @"title": title, @"genre": genre };
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (NSArray *)fileTypes {
|
|
|
|
return @[@"mp3", @"m2a", @"mpa"];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (NSArray *)mimeTypes {
|
|
|
|
return @[@"audio/mpeg", @"audio/x-mp3"];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (NSArray *)fileTypeAssociations {
|
|
|
|
return @[@[@"MPEG Audio File", @"mp3.icns", @"mp3", @"m2a", @"mpa"]];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (float)priority {
|
|
|
|
return 2.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|