/*
* 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
*/
#include "wavpack.h"
static int
get_wavpack_info(PerlIO *infile, char *file, HV *info)
{
wvpinfo *wvp = _wavpack_parse(infile, file, info, 0);
Safefree(wvp);
return 0;
}
wvpinfo *
_wavpack_parse(PerlIO *infile, char *file, HV *info, uint8_t seeking)
{
int err = 0;
int done = 0;
u_char *bptr;
wvpinfo *wvp;
Newz(0, wvp, sizeof(wvpinfo), wvpinfo);
Newz(0, wvp->buf, sizeof(Buffer), Buffer);
Newz(0, wvp->header, sizeof(WavpackHeader), WavpackHeader);
wvp->infile = infile;
wvp->file = file;
wvp->info = info;
wvp->file_offset = 0;
wvp->audio_offset = 0;
wvp->seeking = seeking ? 1 : 0;
buffer_init(wvp->buf, WAVPACK_BLOCK_SIZE);
wvp->file_size = _file_size(infile);
my_hv_store( info, "file_size", newSVuv(wvp->file_size) );
// Loop through each wvpk block until we find a good one
while (!done) {
if ( !_check_buf(infile, wvp->buf, 32, WAVPACK_BLOCK_SIZE) ) {
err = -1;
goto out;
}
bptr = buffer_ptr(wvp->buf);
// If first byte is 'R', assume old version
if ( bptr[0] == 'R' ) {
if ( !_wavpack_parse_old(wvp) ) {
err = -1;
goto out;
}
break;
}
// May need to read past some junk before wvpk header
while ( bptr[0] != 'w' || bptr[1] != 'v' || bptr[2] != 'p' || bptr[3] != 'k' ) {
buffer_consume(wvp->buf, 1);
wvp->audio_offset++;
if ( !buffer_len(wvp->buf) ) {
if ( !_check_buf(infile, wvp->buf, 32, WAVPACK_BLOCK_SIZE) ) {
PerlIO_printf(PerlIO_stderr(), "Unable to find a valid WavPack block in file: %s\n", file);
err = -1;
goto out;
}
}
bptr = buffer_ptr(wvp->buf);
}
if ( _wavpack_parse_block(wvp) ) {
done = 1;
}
}
my_hv_store( info, "audio_offset", newSVuv(wvp->audio_offset) );
out:
buffer_free(wvp->buf);
Safefree(wvp->buf);
Safefree(wvp->header);
return wvp;
}
int
_wavpack_parse_block(wvpinfo *wvp)
{
unsigned char *bptr;
uint16_t remaining;
bptr = buffer_ptr(wvp->buf);
// Verify wvpk signature
if ( bptr[0] != 'w' || bptr[1] != 'v' || bptr[2] != 'p' || bptr[3] != 'k' ) {
DEBUG_TRACE("Invalid wvpk header at %llu\n", wvp->file_offset);
return 1;
}
buffer_consume(wvp->buf, 4);
wvp->header->ckSize = buffer_get_int_le(wvp->buf);
wvp->header->version = buffer_get_short_le(wvp->buf);
wvp->header->track_no = buffer_get_char(wvp->buf);
wvp->header->index_no = buffer_get_char(wvp->buf);
wvp->header->total_samples = buffer_get_int_le(wvp->buf);
wvp->header->block_index = buffer_get_int_le(wvp->buf);
wvp->header->block_samples = buffer_get_int_le(wvp->buf);
wvp->header->flags = buffer_get_int_le(wvp->buf);
wvp->header->crc = buffer_get_int_le(wvp->buf);
DEBUG_TRACE("wvpk header @ %llu:\n", wvp->file_offset);
DEBUG_TRACE(" size: %u\n", wvp->header->ckSize);
DEBUG_TRACE(" version: 0x%x\n", wvp->header->version);
DEBUG_TRACE(" track_no: 0x%x\n", wvp->header->track_no);
DEBUG_TRACE(" index_no: 0x%x\n", wvp->header->index_no);
DEBUG_TRACE(" total_samples: %u\n", wvp->header->total_samples);
DEBUG_TRACE(" block_index: %u\n", wvp->header->block_index);
DEBUG_TRACE(" block_samples: %u\n", wvp->header->block_samples);
DEBUG_TRACE(" flags: 0x%x\n", wvp->header->flags);
DEBUG_TRACE(" crc: 0x%x\n", wvp->header->crc);
wvp->file_offset += 32;
my_hv_store( wvp->info, "encoder_version", newSVuv(wvp->header->version) );
if (wvp->header->version < 0x4) {
// XXX old version and not handled by 'R' check above for old version
PerlIO_printf(PerlIO_stderr(), "Unsupported old WavPack version: 0x%x\n", wvp->header->version);
return 1;
}
// Read data from flags
my_hv_store( wvp->info, "bits_per_sample", newSVuv( 8 * ((wvp->header->flags & 0x3) + 1) ) );
// Encoding mode
my_hv_store( wvp->info, (wvp->header->flags & 0x8) ? "hybrid" : "lossless", newSVuv(1) );
{
// samplerate, may be overridden by a later ID_SAMPLE_RATE metadata block
uint32_t samplerate_index = (wvp->header->flags & 0x7800000) >> 23;
if ( samplerate_index >= 0 && samplerate_index < 0xF ) {
my_hv_store( wvp->info, "samplerate", newSVuv( wavpack_sample_rates[samplerate_index] ) );
}
else {
// Default to 44.1 just in case
my_hv_store( wvp->info, "samplerate", newSVuv(44100) );
}
}
// Channels, may be overridden by a later ID_CHANNEL_INFO metadata block
my_hv_store( wvp->info, "channels", newSVuv( (wvp->header->flags & 0x4) ? 1 : 2 ) );
// Parse metadata sub-blocks
remaining = wvp->header->ckSize - 24; // ckSize is 8 less than the block size
// If block_samples is 0, we need to skip to the next block
if ( !wvp->header->block_samples ) {
wvp->file_offset += remaining;
_wavpack_skip(wvp, remaining);
return 0;
}
while (remaining > 0) {
// Read sub-block header (2-4 bytes)
unsigned char id;
uint32_t size;
DEBUG_TRACE("remaining: %d\n", remaining);
if ( !_check_buf(wvp->infile, wvp->buf, 4, WAVPACK_BLOCK_SIZE) ) {
return 0;
}
id = buffer_get_char(wvp->buf);
remaining--;
// Size is in words
if (id & ID_LARGE) {
// 24-bit large size
id &= ~ID_LARGE;
size = buffer_get_int24_le(wvp->buf) << 1;
remaining -= 3;
DEBUG_TRACE(" ID_LARGE, changed to %x\n", id);
}
else {
// 8-bit size
size = buffer_get_char(wvp->buf) << 1;
remaining--;
}
if (id & ID_ODD_SIZE) {
id &= ~ID_ODD_SIZE;
size--;
DEBUG_TRACE(" ID_ODD_SIZE, changed to %x\n", id);
}
if ( id == ID_WV_BITSTREAM || !size ) {
// Found the bitstream, don't read any farther
DEBUG_TRACE(" Sub-Chunk: WV_BITSTREAM (size %u)\n", size);
break;
}
// We only care about 0x27 (ID_SAMPLE_RATE) and 0xd (ID_CHANNEL_INFO)
switch (id) {
case ID_SAMPLE_RATE:
DEBUG_TRACE(" Sub-Chunk: ID_SAMPLE_RATE (size: %u)\n", size);
_wavpack_parse_sample_rate(wvp, size);
break;
case ID_CHANNEL_INFO:
DEBUG_TRACE(" Sub-Chunk: ID_CHANNEL_INFO (size: %u)\n", size);
_wavpack_parse_channel_info(wvp, size);
break;
default:
// Skip it
DEBUG_TRACE(" Sub-Chunk: %x (size: %u) (skipped)\n", id, size);
_wavpack_skip(wvp, size);
}
remaining -= size;
// If size was odd, skip a byte
if (size & 0x1) {
if ( buffer_len(wvp->buf) ) {
buffer_consume(wvp->buf, 1);
}
else {
_wavpack_skip(wvp, 1);
}
remaining--;
}
}
// Calculate bitrate
if ( wvp->header->total_samples && wvp->file_size > 0 ) {
SV **samplerate = my_hv_fetch( wvp->info, "samplerate" );
if (samplerate != NULL) {
uint32_t song_length_ms = ((wvp->header->total_samples * 1.0) / SvIV(*samplerate)) * 1000;
my_hv_store( wvp->info, "song_length_ms", newSVuv(song_length_ms) );
my_hv_store( wvp->info, "bitrate", newSVuv( _bitrate(wvp->file_size - wvp->audio_offset, song_length_ms) ) );
my_hv_store( wvp->info, "total_samples", newSVuv(wvp->header->total_samples) );
}
}
return 1;
}
int
_wavpack_parse_sample_rate(wvpinfo *wvp, uint32_t size)
{
uint32_t samplerate = buffer_get_int24_le(wvp->buf);
my_hv_store( wvp->info, "samplerate", newSVuv(samplerate) );
return 1;
}
int
_wavpack_parse_channel_info(wvpinfo *wvp, uint32_t size)
{
uint32_t channels;
unsigned char *bptr = buffer_ptr(wvp->buf);
if (size == 6) {
channels = (bptr[0] | ((bptr[2] & 0xf) << 8)) + 1;
}
else {
channels = bptr[0];
}
my_hv_store( wvp->info, "channels", newSVuv(channels) );
buffer_consume(wvp->buf, size);
return 1;
}
void
_wavpack_skip(wvpinfo *wvp, uint32_t size)
{
if ( buffer_len(wvp->buf) >= size ) {
//buffer_dump(mp4->buf, size);
buffer_consume(wvp->buf, size);
DEBUG_TRACE(" skipped buffer data size %d\n", size);
}
else {
PerlIO_seek(wvp->infile, size - buffer_len(wvp->buf), SEEK_CUR);
buffer_clear(wvp->buf);
DEBUG_TRACE(" seeked past %d bytes to %d\n", size, (int)PerlIO_tell(wvp->infile));
}
}
int
_wavpack_parse_old(wvpinfo *wvp)
{
int ret = 1;
char chunk_id[5];
uint32_t chunk_size;
WavpackHeader3 wphdr;
WaveHeader3 wavhdr;
unsigned char *bptr;
uint32_t total_samples;
uint32_t song_length_ms;
Zero(&wavhdr, sizeof(wavhdr), char);
Zero(&wphdr, sizeof(wphdr), char);
DEBUG_TRACE("Parsing old WavPack version\n");
// Verify RIFF header
if ( strncmp( (char *)buffer_ptr(wvp->buf), "RIFF", 4 ) ) {
PerlIO_printf(PerlIO_stderr(), "Invalid WavPack file: missing RIFF header: %s\n", wvp->file);
ret = 0;
goto out;
}
buffer_consume(wvp->buf, 4);
chunk_size = buffer_get_int_le(wvp->buf);
// Check format
if ( strncmp( (char *)buffer_ptr(wvp->buf), "WAVE", 4 ) ) {
PerlIO_printf(PerlIO_stderr(), "Invalid WavPack file: missing WAVE header: %s\n", wvp->file);
ret = 0;
goto out;
}
buffer_consume(wvp->buf, 4);
wvp->file_offset += 12;
// Verify we have at least 8 bytes
if ( !_check_buf(wvp->infile, wvp->buf, 8, WAVPACK_BLOCK_SIZE) ) {
ret = 0;
goto out;
}
// loop through all chunks, read fmt, and break at data
while ( buffer_len(wvp->buf) >= 8 ) {
strncpy( chunk_id, (char *)buffer_ptr(wvp->buf), 4 );
chunk_id[4] = '\0';
buffer_consume(wvp->buf, 4);
chunk_size = buffer_get_int_le(wvp->buf);
wvp->file_offset += 8;
// Adjust for padding
if ( chunk_size % 2 ) {
chunk_size++;
}
DEBUG_TRACE(" %s size %d\n", chunk_id, chunk_size);
if ( !strcmp( chunk_id, "data" ) ) {
break;
}
wvp->file_offset += chunk_size;
if ( !strcmp( chunk_id, "fmt " ) ) {
if ( !_check_buf(wvp->infile, wvp->buf, chunk_size, WAV_BLOCK_SIZE) ) {
ret = 0;
goto out;
}
if (chunk_size < sizeof(wavhdr)) {
ret = 0;
goto out;
}
// Read wav header
wavhdr.FormatTag = buffer_get_short_le(wvp->buf);
wavhdr.NumChannels = buffer_get_short_le(wvp->buf);
wavhdr.SampleRate = buffer_get_int_le(wvp->buf);
wavhdr.BytesPerSecond = buffer_get_int_le(wvp->buf);
wavhdr.BlockAlign = buffer_get_short_le(wvp->buf);
wavhdr.BitsPerSample = buffer_get_short_le(wvp->buf);
// Skip rest of fmt chunk if necessary
if (chunk_size > 16) {
_wavpack_skip(wvp, chunk_size - 16);
}
}
else {
// Skip it
_wavpack_skip(wvp, chunk_size);
}
// Verify we have at least 8 bytes
if ( !_check_buf(wvp->infile, wvp->buf, 8, WAVPACK_BLOCK_SIZE) ) {
ret = 0;
goto out;
}
}
// Verify wav header, this code comes from unpack3.c
if (
wavhdr.FormatTag != 1 || !wavhdr.NumChannels || wavhdr.NumChannels > 2 ||
!wavhdr.SampleRate || wavhdr.BitsPerSample < 16 || wavhdr.BitsPerSample > 24 ||
wavhdr.BlockAlign / wavhdr.NumChannels > 3 || wavhdr.BlockAlign % wavhdr.NumChannels ||
wavhdr.BlockAlign / wavhdr.NumChannels < (wavhdr.BitsPerSample + 7) / 8
) {
ret = 0;
goto out;
}
// chunk_size here is the size of the data chunk
total_samples = chunk_size / wavhdr.NumChannels / ((wavhdr.BitsPerSample > 16) ? 3 : 2);
// read WavpackHeader3 (differs for each version)
bptr = buffer_ptr(wvp->buf);
if ( bptr[0] != 'w' || bptr[1] != 'v' || bptr[2] != 'p' || bptr[3] != 'k' ) {
PerlIO_printf(PerlIO_stderr(), "Invalid WavPack file: missing wvpk header: %s\n", wvp->file);
ret = 0;
goto out;
}
buffer_consume(wvp->buf, 4);
wphdr.ckSize = buffer_get_int_le(wvp->buf);
wphdr.version = buffer_get_short_le(wvp->buf);
if (wphdr.version >= 2) {
wphdr.bits = buffer_get_short_le(wvp->buf);
}
if (wphdr.version == 3) {
wphdr.flags = buffer_get_short_le(wvp->buf);
wphdr.shift = buffer_get_short_le(wvp->buf);
wphdr.total_samples = buffer_get_int_le(wvp->buf);
total_samples = wphdr.total_samples;
}
DEBUG_TRACE("wvpk header @ %llu:\n", wvp->file_offset);
DEBUG_TRACE(" size: %u\n", wphdr.ckSize);
DEBUG_TRACE(" version: %d\n", wphdr.version);
DEBUG_TRACE(" bits: 0x%x\n", wphdr.bits);
DEBUG_TRACE(" flags: 0x%x\n", wphdr.flags);
DEBUG_TRACE(" shift: 0x%x\n", wphdr.shift);
DEBUG_TRACE(" total_samples: %d\n", wphdr.total_samples);
my_hv_store( wvp->info, "encoder_version", newSVuv(wphdr.version) );
my_hv_store( wvp->info, "bits_per_sample", newSVuv(wavhdr.BitsPerSample) );
my_hv_store( wvp->info, "channels", newSVuv(wavhdr.NumChannels) );
my_hv_store( wvp->info, "samplerate", newSVuv(wavhdr.SampleRate) );
my_hv_store( wvp->info, "total_samples", newSVuv(total_samples) );
song_length_ms = ((total_samples * 1.0) / wavhdr.SampleRate) * 1000;
my_hv_store( wvp->info, "song_length_ms", newSVuv(song_length_ms) );
my_hv_store( wvp->info, "bitrate", newSVuv( _bitrate(wvp->file_size - wvp->audio_offset, song_length_ms) ) );
out:
return ret;
}