/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* This file is derived from Audio::FLAC::Header by Dan Sully.
*
* CRC code comes from libFLAC/crc.c (included here because it's not a public API function)
*/
#include "flac.h"
/* frame header size (16 bytes) + 4608 stereo 16-bit samples (higher than 4608 is possible, but not done) */
#define FLAC_FRAME_MAX_BLOCK 18448
#define FLAC_HEADER_LEN 16
/* CRC-8, poly = x^8 + x^2 + x^1 + x^0, init = 0 */
FLAC__byte const my_FLAC__crc8_table[256] = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
};
FLAC__uint8 my_FLAC__crc8(const FLAC__byte *data, unsigned len) {
FLAC__uint8 crc = 0;
while(len--)
crc = my_FLAC__crc8_table[crc ^ *data++];
return crc;
}
void _cuesheet_frame_to_msf(unsigned frame, unsigned *minutes, unsigned *seconds, unsigned *frames) {
*frames = frame % 75;
frame /= 75;
*seconds = frame % 60;
frame /= 60;
*minutes = frame;
}
void _read_metadata(char *path, HV *info, HV *tags, FLAC__StreamMetadata *block, unsigned block_number) {
unsigned i;
int storePicture = 0;
HV *pictureContainer = newHV();
AV *allpicturesContainer = NULL;
switch (block->type) {
case FLAC__METADATA_TYPE_STREAMINFO:
{
float totalSeconds;
my_hv_store(info, "minimum_blocksize", newSVuv(block->data.stream_info.min_blocksize));
my_hv_store(info, "maximum_blocksize", newSVuv(block->data.stream_info.max_blocksize));
my_hv_store(info, "minimum_framesize", newSVuv(block->data.stream_info.min_framesize));
my_hv_store(info, "maximum_framesize", newSVuv(block->data.stream_info.max_framesize));
my_hv_store(info, "samplerate", newSVuv(block->data.stream_info.sample_rate));
my_hv_store(info, "channels", newSVuv(block->data.stream_info.channels));
my_hv_store(info, "bits_per_sample", newSVuv(block->data.stream_info.bits_per_sample));
my_hv_store(info, "total_samples", newSVnv(block->data.stream_info.total_samples));
if (block->data.stream_info.md5sum != NULL) {
/* Initialize an SV with the first element,
and then append to it. If we don't do it this way, we get a "use of
uninitialized element" in subroutine warning. */
SV *md5 = newSVpvf("%02x", (unsigned)block->data.stream_info.md5sum[0]);
for (i = 1; i < 16; i++) {
sv_catpvf(md5, "%02x", (unsigned)block->data.stream_info.md5sum[i]);
}
my_hv_store(info, "md5", md5);
}
/* Store some other metadata for backwards compatability with the original Audio::FLAC */
/* needs to be higher resolution */
totalSeconds = block->data.stream_info.total_samples / (float)block->data.stream_info.sample_rate;
if (totalSeconds <= 0) {
PerlIO_printf(PerlIO_stderr(), "File: %s - %s\n%s\n",
path,
"totalSeconds is 0 - we couldn't find either TOTALSAMPLES or SAMPLERATE!",
"setting totalSeconds to 1 to avoid divide by zero error!"
);
totalSeconds = 1;
}
my_hv_store(info, "song_length_ms", newSViv(totalSeconds * 1000));
my_hv_store(info, "frames", newSVnv((totalSeconds - (int)totalSeconds) * 75));
break;
}
case FLAC__METADATA_TYPE_PADDING:
case FLAC__METADATA_TYPE_SEEKTABLE:
/* Don't handle these yet. */
break;
case FLAC__METADATA_TYPE_APPLICATION:
{
if (block->data.application.id[0]) {
HV *app = newHV();
SV *tmpId = newSVpvf("%02x", (unsigned)block->data.application.id[0]);
SV *appId;
for (i = 1; i < 4; i++) {
sv_catpvf(tmpId, "%02x", (unsigned)block->data.application.id[i]);
}
// Be compatible with the pure perl version
appId = newSVpvf("%ld", strtol(SvPV_nolen(tmpId), NULL, 16));
if (block->data.application.data != 0) {
my_hv_store_ent(app, appId, newSVpvn((char*)block->data.application.data, block->length));
}
my_hv_store(tags, "APPLICATION", newRV_noinc((SV*) app));
SvREFCNT_dec(tmpId);
SvREFCNT_dec(appId);
}
break;
}
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
{
if (block->data.vorbis_comment.vendor_string.entry) {
my_hv_store(tags, "VENDOR", newSVpv((char*)block->data.vorbis_comment.vendor_string.entry, 0));
}
for (i = 0; i < block->data.vorbis_comment.num_comments; i++) {
_split_vorbis_comment((char*)block->data.vorbis_comment.comments[i].entry, tags);
}
break;
}
case FLAC__METADATA_TYPE_CUESHEET:
{
AV *cueArray = newAV();
/*
* buffer for decimal representations of uint64_t values
*
* newSVpvf() and sv_catpvf() can't handle 64-bit values
* in some cases, so we need to do the conversion "manually"
* with sprintf() and the PRIu64 format macro for portability
*
* see http://bugs.debian.org/462249
*
* maximum string length: ceil(log10(2**64)) == 20 (+trailing \0)
*/
char decimal[21];
/* A lot of this comes from flac/src/share/grabbag/cuesheet.c */
const FLAC__StreamMetadata_CueSheet *cs;
unsigned track_num, index_num;
cs = &block->data.cue_sheet;
if (*(cs->media_catalog_number)) {
av_push(cueArray, newSVpvf("CATALOG %s\n", cs->media_catalog_number));
}
av_push(cueArray, newSVpvf("FILE \"%s\" FLAC\n", path));
for (track_num = 0; track_num < cs->num_tracks-1; track_num++) {
const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num;
av_push(cueArray, newSVpvf(" TRACK %02u %s\n",
(unsigned)track->number, track->type == 0? "AUDIO" : "DATA"
));
if (track->pre_emphasis) {
av_push(cueArray, newSVpv(" FLAGS PRE\n", 0));
}
if (*(track->isrc)) {
av_push(cueArray, newSVpvf(" ISRC %s\n", track->isrc));
}
for (index_num = 0; index_num < track->num_indices; index_num++) {
const FLAC__StreamMetadata_CueSheet_Index *index = track->indices + index_num;
SV *indexSV = newSVpvf(" INDEX %02u ", (unsigned)index->number);
if (cs->is_cd) {
unsigned logical_frame = (unsigned)((track->offset + index->offset) / (44100 / 75));
unsigned m, s, f;
_cuesheet_frame_to_msf(logical_frame, &m, &s, &f);
sv_catpvf(indexSV, "%02u:%02u:%02u\n", m, s, f);
} else {
sprintf(decimal, "%"PRIu64, track->offset + index->offset);
sv_catpvf(indexSV, "%s\n", decimal);
}
av_push(cueArray, indexSV);
}
}
sprintf(decimal, "%"PRIu64, cs->lead_in);
av_push(cueArray, newSVpvf("REM FLAC__lead-in %s\n", decimal));
sprintf(decimal, "%"PRIu64, cs->tracks[track_num].offset);
av_push(cueArray, newSVpvf("REM FLAC__lead-out %u %s\n",
(unsigned)cs->tracks[track_num].number, decimal)
);
my_hv_store(tags, "CUESHEET", newRV_noinc((SV*) cueArray));
break;
}
/* The PICTURE metadata block came about in FLAC 1.1.3 */
#ifdef FLAC_API_VERSION_CURRENT
case FLAC__METADATA_TYPE_PICTURE:
{
HV *picture = newHV();
SV *type;
my_hv_store(picture, "mime_type", newSVpv(block->data.picture.mime_type, 0));
my_hv_store(picture, "description", newSVpv((const char*)block->data.picture.description, 0));
my_hv_store(picture, "width", newSViv(block->data.picture.width));
my_hv_store(picture, "height", newSViv(block->data.picture.height));
my_hv_store(picture, "depth", newSViv(block->data.picture.depth));
my_hv_store(picture, "color_index", newSViv(block->data.picture.colors));
my_hv_store(picture, "image_data", newSVpv((const char*)block->data.picture.data, block->data.picture.data_length));
my_hv_store(picture, "picture_type", newSViv(block->data.picture.type));
type = newSViv(block->data.picture.type);
my_hv_store_ent(
pictureContainer,
type,
newRV_noinc((SV*) picture)
);
SvREFCNT_dec(type);
storePicture = 1;
/* update allpictures */
if (my_hv_exists(tags, "ALLPICTURES")) {
allpicturesContainer = (AV *) SvRV(*my_hv_fetch(tags, "ALLPICTURES"));
} else {
allpicturesContainer = newAV();
/* store the 'allpictures' array */
my_hv_store(tags, "ALLPICTURES", newRV_noinc((SV*) allpicturesContainer));
}
av_push(allpicturesContainer, (SV*) newRV((SV*) picture));
break;
}
#endif
/* XXX- Just ignore for now */
default:
break;
}
/* store the 'picture' hash */
if (storePicture && hv_scalar(pictureContainer)) {
my_hv_store(tags, "PICTURE", newRV_noinc((SV*) pictureContainer));
} else {
SvREFCNT_dec((SV*) pictureContainer);
}
}
/* From src/metaflac/operations.c */
void print_error_with_chain_status(FLAC__Metadata_Chain *chain, const char *format, ...) {
const FLAC__Metadata_ChainStatus status = FLAC__metadata_chain_status(chain);
va_list args;
FLAC__ASSERT(0 != format);
va_start(args, format);
(void) PerlIO_vprintf(PerlIO_stderr(), format, args);
va_end(args);
PerlIO_printf(PerlIO_stderr(), "status = \"%s\"\n", FLAC__Metadata_ChainStatusString[status]);
if (status == FLAC__METADATA_CHAIN_STATUS_ERROR_OPENING_FILE) {
PerlIO_printf(PerlIO_stderr(), "The FLAC file could not be opened. Most likely the file does not exist or is not readable.");
} else if (status == FLAC__METADATA_CHAIN_STATUS_NOT_A_FLAC_FILE) {
PerlIO_printf(PerlIO_stderr(), "The file does not appear to be a FLAC file.");
} else if (status == FLAC__METADATA_CHAIN_STATUS_NOT_WRITABLE) {
PerlIO_printf(PerlIO_stderr(), "The FLAC file does not have write permissions.");
} else if (status == FLAC__METADATA_CHAIN_STATUS_BAD_METADATA) {
PerlIO_printf(PerlIO_stderr(), "The metadata to be writted does not conform to the FLAC metadata specifications.");
} else if (status == FLAC__METADATA_CHAIN_STATUS_READ_ERROR) {
PerlIO_printf(PerlIO_stderr(), "There was an error while reading the FLAC file.");
} else if (status == FLAC__METADATA_CHAIN_STATUS_WRITE_ERROR) {
PerlIO_printf(PerlIO_stderr(), "There was an error while writing FLAC file; most probably the disk is full.");
} else if (status == FLAC__METADATA_CHAIN_STATUS_UNLINK_ERROR) {
PerlIO_printf(PerlIO_stderr(), "There was an error removing the temporary FLAC file.");
}
}
static int
get_flac_metadata(PerlIO *infile, char *file, HV *info, HV *tags)
{
FLAC__Metadata_Chain *chain = FLAC__metadata_chain_new();
if (chain == 0) {
PerlIO_printf(PerlIO_stderr(), "Out of memory allocating chain. Cannot open %s\n", file);
return -1;
}
if (!FLAC__metadata_chain_read(chain, file)) {
print_error_with_chain_status(chain, "%s: ERROR: reading metadata", file);
FLAC__metadata_chain_delete(chain);
return -1;
}
{
FLAC__Metadata_Iterator *iterator = FLAC__metadata_iterator_new();
FLAC__StreamMetadata *block = 0;
FLAC__bool ok = true;
unsigned block_number = 0;
if (iterator == 0) {
PerlIO_printf(PerlIO_stderr(), "Out of memory allocating iterator. Cannot open %s\n", file);
FLAC__metadata_chain_delete(chain);
return -1;
}
FLAC__metadata_iterator_init(iterator, chain);
do {
block = FLAC__metadata_iterator_get_block(iterator);
ok &= (0 != block);
if (!ok) {
PerlIO_printf(PerlIO_stderr(), "%s: ERROR: couldn't get block from chain.\n", file);
} else {
_read_metadata(file, info, tags, block, block_number);
}
block_number++;
} while (ok && FLAC__metadata_iterator_next(iterator));
FLAC__metadata_iterator_delete(iterator);
}
FLAC__metadata_chain_delete(chain);
{
/* Find the offset of the start pos for audio blocks (ie: after metadata) */
unsigned int is_last = 0;
unsigned char buf[4];
long len;
struct stat st;
float totalMS;
if (PerlIO_read(infile, &buf, 4) == -1) {
PerlIO_printf(PerlIO_stderr(), "Couldn't read magic fLaC header! %s\n", strerror(errno));
return -1;
}
if (memcmp(buf, ID3HEADERFLAG, 3) == 0) {
unsigned id3size = 0;
int c = 0;
/* How big is the ID3 header? Skip the next two bytes */
if (PerlIO_read(infile, &buf, 2) == -1) {
PerlIO_printf(PerlIO_stderr(), "Couldn't read ID3 header length! %s\n", strerror(errno));
return -1;
}
/* The size of the ID3 tag is a 'synchsafe' 4-byte uint */
for (c = 0; c < 4; c++) {
if (PerlIO_read(infile, &buf, 1) == -1 || buf[0] & 0x80) {
PerlIO_printf(PerlIO_stderr(), "Couldn't read ID3 header length (syncsafe)! %s\n", strerror(errno));
return -1;
}
id3size <<= 7;
id3size |= (buf[0] & 0x7f);
}
if (PerlIO_seek(infile, id3size, SEEK_CUR) < 0) {
PerlIO_printf(PerlIO_stderr(), "Couldn't seek past ID3 header!\n");
return -1;
}
if (PerlIO_read(infile, &buf, 4) == -1) {
PerlIO_printf(PerlIO_stderr(), "Couldn't read magic fLaC header! %s\n", strerror(errno));
return -1;
}
}
if (memcmp(buf, FLACHEADERFLAG, 4)) {
PerlIO_printf(PerlIO_stderr(), "Couldn't read magic fLaC header - got gibberish instead!\n");
return -1;
}
while (!is_last) {
if (PerlIO_read(infile, &buf, 4) == -1) {
PerlIO_printf(PerlIO_stderr(), "Couldn't read 4 bytes of the metadata block!\n");
return -1;
}
is_last = (unsigned int)(buf[0] & 0x80);
len = (long)((buf[1] << 16) | (buf[2] << 8) | (buf[3]));
PerlIO_seek(infile, len, SEEK_CUR);
}
len = PerlIO_tell(infile);
my_hv_store(info, "audio_offset", newSVnv(len));
/* Now calculate the bit rate and file size */
if (my_hv_exists(info, "song_length_ms")) {
totalMS = (float)SvIV(*(my_hv_fetch(info, "song_length_ms")));
/* Find the file size */
if (stat(file, &st) == 0) {
my_hv_store(info, "file_size", newSViv(st.st_size));
} else {
PerlIO_printf(PerlIO_stderr(), "Couldn't stat file: [%s], might be more problems ahead!", file);
}
my_hv_store(info, "bitrate", newSVnv(8 * (st.st_size - len) / (totalMS / 1000) ));
}
}
return 0;
}
bool
_is_flac_header(unsigned char *buf)
{
FLAC__uint32 sync1, sync2, block_size, sample_rate, channel, sample_size, padding;
size_t len = 4;
FLAC__byte crc8;
sync1 = buf[0];
sync2 = buf[1] >> 2;
block_size = buf[2] >> 4;
sample_rate = buf[2] & 0x0F;
channel = buf[3] >> 4;
sample_size = (buf[3] >> 1) & 0x07;
padding = buf[3] & 0x1;
if (sync1 != 0xFF || sync2 != 0x3E || sample_rate == 0xF || channel > 0xC || sample_size == 0x3 || sample_size == 0x7 || padding) {
return false;
}
/*
fprintf(stderr, "Found FLAC header: block_size: %d, sample_rate: %d, channel: %d, sample_size: %d, padding: %d\n",
block_size, sample_rate, channel, sample_size, padding
);
*/
if (!(buf[4] & 0x80)) {
len += 1;
} else if(buf[4] & 0xC0 && !(buf[4] & 0x20)) {
len += 2;
} else if(buf[4] & 0xE0 && !(buf[4] & 0x10)) {
len += 3;
} else if(buf[4] & 0xF0 && !(buf[4] & 0x08)) {
len += 4;
} else if(buf[4] & 0xF8 && !(buf[4] & 0x04)) {
len += 5;
} else if(buf[4] & 0xFC && !(buf[4] & 0x02)) {
len += 6;
} else if(buf[4] & 0xFE && !(buf[4] & 0x01)) {
len += 7;
}
// Block size can be stored at the end of the header
if (block_size == 0x6) {
len += 1;
} else if (block_size == 0x7) {
len += 2;
}
// Sample rate can be stored at the end of the header
if (sample_rate == 0xC) {
len += 1;
} else if (sample_rate == 0xD || sample_rate == 0xE) {
len += 2;
} else if (block_size == 0xD || block_size == 0xE) {
// XXX: I'm not sure why this is part of the sample_rate if block, bug?
len += 2;
}
crc8 = buf[len];
if (my_FLAC__crc8(buf, len) != crc8) {
fprintf(stderr, "CRC FAILED\n");
return false;
}
return true;
}
static int
flac_find_frame(PerlIO *infile, char *file, int offset)
{
Buffer flac_buf;
unsigned char *bptr;
unsigned int buf_size;
int frame_offset = -1;
FLAC__uint64 pos = 0;
PerlIO_seek(infile, offset, SEEK_SET);
buffer_init(&flac_buf, FLAC_FRAME_MAX_BLOCK);
if (!_check_buf(infile, &flac_buf, FLAC_FRAME_MAX_BLOCK, FLAC_FRAME_MAX_BLOCK)) {
goto out;
}
bptr = (unsigned char *)buffer_ptr(&flac_buf);
buf_size = buffer_len(&flac_buf);
for (pos = 0; pos != (buf_size - FLAC_HEADER_LEN); pos += 1) {
if (bptr[pos] != 0xFF)
continue;
if (!_is_flac_header(&bptr[pos]))
continue;
frame_offset = offset + pos;
break;
}
out:
buffer_free(&flac_buf);
return frame_offset;
}