439 lines
13 KiB
C++
Executable File
439 lines
13 KiB
C++
Executable File
/**********
|
|
This library is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU Lesser General Public License as published by the
|
|
Free Software Foundation; either version 2.1 of the License, or (at your
|
|
option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.)
|
|
|
|
This library 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 Lesser General Public License for
|
|
more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public License
|
|
along with this library; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
**********/
|
|
// "liveMedia"
|
|
// Copyright (c) 1996-2016 Live Networks, Inc. All rights reserved.
|
|
// A class encapsulating the state of a MP3 stream
|
|
// Implementation
|
|
|
|
#include "MP3StreamState.hh"
|
|
#include "InputFile.hh"
|
|
#include "GroupsockHelper.hh"
|
|
|
|
#if defined(__WIN32__) || defined(_WIN32)
|
|
#define snprintf _snprintf
|
|
#if _MSC_VER >= 1400 // 1400 == vs2005
|
|
#define fileno _fileno
|
|
#endif
|
|
#endif
|
|
|
|
#define MILLION 1000000
|
|
|
|
MP3StreamState::MP3StreamState(UsageEnvironment& env)
|
|
: fEnv(env), fFid(NULL), fPresentationTimeScale(1) {
|
|
}
|
|
|
|
MP3StreamState::~MP3StreamState() {
|
|
// Close our open file or socket:
|
|
if (fFid != NULL && fFid != stdin) {
|
|
if (fFidIsReallyASocket) {
|
|
intptr_t fid_long = (intptr_t)fFid;
|
|
closeSocket((int)fid_long);
|
|
} else {
|
|
CloseInputFile(fFid);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MP3StreamState::assignStream(FILE* fid, unsigned fileSize) {
|
|
fFid = fid;
|
|
|
|
if (fileSize == (unsigned)(-1)) { /*HACK#####*/
|
|
fFidIsReallyASocket = 1;
|
|
fFileSize = 0;
|
|
} else {
|
|
fFidIsReallyASocket = 0;
|
|
fFileSize = fileSize;
|
|
}
|
|
fNumFramesInFile = 0; // until we know otherwise
|
|
fIsVBR = fHasXingTOC = False; // ditto
|
|
|
|
// Set the first frame's 'presentation time' to the current wall time:
|
|
gettimeofday(&fNextFramePresentationTime, NULL);
|
|
}
|
|
|
|
struct timeval MP3StreamState::currentFramePlayTime() const {
|
|
unsigned const numSamples = 1152;
|
|
unsigned const freq = fr().samplingFreq*(1 + fr().isMPEG2);
|
|
|
|
// result is numSamples/freq
|
|
unsigned const uSeconds
|
|
= ((numSamples*2*MILLION)/freq + 1)/2; // rounds to nearest integer
|
|
|
|
struct timeval result;
|
|
result.tv_sec = uSeconds/MILLION;
|
|
result.tv_usec = uSeconds%MILLION;
|
|
return result;
|
|
}
|
|
|
|
float MP3StreamState::filePlayTime() const {
|
|
unsigned numFramesInFile = fNumFramesInFile;
|
|
if (numFramesInFile == 0) {
|
|
// Estimate the number of frames from the file size, and the
|
|
// size of the current frame:
|
|
numFramesInFile = fFileSize/(4 + fCurrentFrame.frameSize);
|
|
}
|
|
|
|
struct timeval const pt = currentFramePlayTime();
|
|
return numFramesInFile*(pt.tv_sec + pt.tv_usec/(float)MILLION);
|
|
}
|
|
|
|
unsigned MP3StreamState::getByteNumberFromPositionFraction(float fraction) {
|
|
if (fHasXingTOC) {
|
|
// The file is VBR, with a Xing TOC; use it to determine which byte to seek to:
|
|
float percent = fraction*100.0f;
|
|
unsigned a = (unsigned)percent;
|
|
if (a > 99) a = 99;
|
|
|
|
unsigned fa = fXingTOC[a];
|
|
unsigned fb;
|
|
if (a < 99) {
|
|
fb = fXingTOC[a+1];
|
|
} else {
|
|
fb = 256;
|
|
}
|
|
fraction = (fa + (fb-fa)*(percent-a))/256.0f;
|
|
}
|
|
|
|
return (unsigned)(fraction*fFileSize);
|
|
}
|
|
|
|
void MP3StreamState::seekWithinFile(unsigned seekByteNumber) {
|
|
if (fFidIsReallyASocket) return; // it's not seekable
|
|
|
|
SeekFile64(fFid, seekByteNumber, SEEK_SET);
|
|
}
|
|
|
|
unsigned MP3StreamState::findNextHeader(struct timeval& presentationTime) {
|
|
presentationTime = fNextFramePresentationTime;
|
|
|
|
if (!findNextFrame()) return 0;
|
|
|
|
// From this frame, figure out the *next* frame's presentation time:
|
|
struct timeval framePlayTime = currentFramePlayTime();
|
|
if (fPresentationTimeScale > 1) {
|
|
// Scale this value
|
|
unsigned secondsRem = framePlayTime.tv_sec % fPresentationTimeScale;
|
|
framePlayTime.tv_sec -= secondsRem;
|
|
framePlayTime.tv_usec += secondsRem*MILLION;
|
|
framePlayTime.tv_sec /= fPresentationTimeScale;
|
|
framePlayTime.tv_usec /= fPresentationTimeScale;
|
|
}
|
|
fNextFramePresentationTime.tv_usec += framePlayTime.tv_usec;
|
|
fNextFramePresentationTime.tv_sec
|
|
+= framePlayTime.tv_sec + fNextFramePresentationTime.tv_usec/MILLION;
|
|
fNextFramePresentationTime.tv_usec %= MILLION;
|
|
|
|
return fr().hdr;
|
|
}
|
|
|
|
Boolean MP3StreamState::readFrame(unsigned char* outBuf, unsigned outBufSize,
|
|
unsigned& resultFrameSize,
|
|
unsigned& resultDurationInMicroseconds) {
|
|
/* We assume that "mp3FindNextHeader()" has already been called */
|
|
|
|
resultFrameSize = 4 + fr().frameSize;
|
|
|
|
if (outBufSize < resultFrameSize) {
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr, "Insufficient buffer size for reading input frame (%d, need %d)\n",
|
|
outBufSize, resultFrameSize);
|
|
#endif
|
|
if (outBufSize < 4) outBufSize = 0;
|
|
resultFrameSize = outBufSize;
|
|
|
|
return False;
|
|
}
|
|
|
|
if (resultFrameSize >= 4) {
|
|
unsigned& hdr = fr().hdr;
|
|
*outBuf++ = (unsigned char)(hdr>>24);
|
|
*outBuf++ = (unsigned char)(hdr>>16);
|
|
*outBuf++ = (unsigned char)(hdr>>8);
|
|
*outBuf++ = (unsigned char)(hdr);
|
|
|
|
memmove(outBuf, fr().frameBytes, resultFrameSize-4);
|
|
}
|
|
|
|
struct timeval const pt = currentFramePlayTime();
|
|
resultDurationInMicroseconds = pt.tv_sec*MILLION + pt.tv_usec;
|
|
|
|
return True;
|
|
}
|
|
|
|
void MP3StreamState::getAttributes(char* buffer, unsigned bufferSize) const {
|
|
char const* formatStr
|
|
= "bandwidth %d MPEGnumber %d MPEGlayer %d samplingFrequency %d isStereo %d playTime %d isVBR %d";
|
|
unsigned fpt = (unsigned)(filePlayTime() + 0.5); // rounds to nearest integer
|
|
#if defined(IRIX) || defined(ALPHA) || defined(_QNX4) || defined(IMN_PIM) || defined(CRIS)
|
|
/* snprintf() isn't defined, so just use sprintf() - ugh! */
|
|
sprintf(buffer, formatStr,
|
|
fr().bitrate, fr().isMPEG2 ? 2 : 1, fr().layer, fr().samplingFreq, fr().isStereo,
|
|
fpt, fIsVBR);
|
|
#else
|
|
snprintf(buffer, bufferSize, formatStr,
|
|
fr().bitrate, fr().isMPEG2 ? 2 : 1, fr().layer, fr().samplingFreq, fr().isStereo,
|
|
fpt, fIsVBR);
|
|
#endif
|
|
}
|
|
|
|
// This is crufty old code that needs to be cleaned up #####
|
|
#define HDRCMPMASK 0xfffffd00
|
|
|
|
Boolean MP3StreamState::findNextFrame() {
|
|
unsigned char hbuf[8];
|
|
unsigned l; int i;
|
|
int attempt = 0;
|
|
|
|
read_again:
|
|
if (readFromStream(hbuf, 4) != 4) return False;
|
|
|
|
fr().hdr = ((unsigned long) hbuf[0] << 24)
|
|
| ((unsigned long) hbuf[1] << 16)
|
|
| ((unsigned long) hbuf[2] << 8)
|
|
| (unsigned long) hbuf[3];
|
|
|
|
#ifdef DEBUG_PARSE
|
|
fprintf(stderr, "fr().hdr: 0x%08x\n", fr().hdr);
|
|
#endif
|
|
if (fr().oldHdr != fr().hdr || !fr().oldHdr) {
|
|
i = 0;
|
|
init_resync:
|
|
#ifdef DEBUG_PARSE
|
|
fprintf(stderr, "init_resync: fr().hdr: 0x%08x\n", fr().hdr);
|
|
#endif
|
|
if ( (fr().hdr & 0xffe00000) != 0xffe00000
|
|
|| (fr().hdr & 0x00060000) == 0 // undefined 'layer' field
|
|
|| (fr().hdr & 0x0000F000) == 0 // 'free format' bitrate index
|
|
|| (fr().hdr & 0x0000F000) == 0x0000F000 // undefined bitrate index
|
|
|| (fr().hdr & 0x00000C00) == 0x00000C00 // undefined frequency index
|
|
|| (fr().hdr & 0x00000003) != 0x00000000 // 'emphasis' field unexpectedly set
|
|
) {
|
|
/* RSF: Do the following test even if we're not at the
|
|
start of the file, in case we have two or more
|
|
separate MP3 files cat'ed together:
|
|
*/
|
|
/* Check for RIFF hdr */
|
|
if (fr().hdr == ('R'<<24)+('I'<<16)+('F'<<8)+'F') {
|
|
unsigned char buf[70 /*was: 40*/];
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr,"Skipped RIFF header\n");
|
|
#endif
|
|
readFromStream(buf, 66); /* already read 4 */
|
|
goto read_again;
|
|
}
|
|
/* Check for ID3 hdr */
|
|
if ((fr().hdr&0xFFFFFF00) == ('I'<<24)+('D'<<16)+('3'<<8)) {
|
|
unsigned tagSize, bytesToSkip;
|
|
unsigned char buf[1000];
|
|
readFromStream(buf, 6); /* already read 4 */
|
|
tagSize = ((buf[2]&0x7F)<<21) + ((buf[3]&0x7F)<<14) + ((buf[4]&0x7F)<<7) + (buf[5]&0x7F);
|
|
bytesToSkip = tagSize;
|
|
while (bytesToSkip > 0) {
|
|
unsigned bytesToRead = sizeof buf;
|
|
if (bytesToRead > bytesToSkip) {
|
|
bytesToRead = bytesToSkip;
|
|
}
|
|
readFromStream(buf, bytesToRead);
|
|
bytesToSkip -= bytesToRead;
|
|
}
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr,"Skipped %d-byte ID3 header\n", tagSize);
|
|
#endif
|
|
goto read_again;
|
|
}
|
|
/* give up after 20,000 bytes */
|
|
if (i++ < 20000/*4096*//*1024*/) {
|
|
memmove (&hbuf[0], &hbuf[1], 3);
|
|
if (readFromStream(hbuf+3,1) != 1) {
|
|
return False;
|
|
}
|
|
fr().hdr <<= 8;
|
|
fr().hdr |= hbuf[3];
|
|
fr().hdr &= 0xffffffff;
|
|
#ifdef DEBUG_PARSE
|
|
fprintf(stderr, "calling init_resync %d\n", i);
|
|
#endif
|
|
goto init_resync;
|
|
}
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr,"Giving up searching valid MPEG header\n");
|
|
#endif
|
|
return False;
|
|
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr,"Illegal Audio-MPEG-Header 0x%08lx at offset 0x%lx.\n",
|
|
fr().hdr,tell_stream(str)-4);
|
|
#endif
|
|
/* Read more bytes until we find something that looks
|
|
reasonably like a valid header. This is not a
|
|
perfect strategy, but it should get us back on the
|
|
track within a short time (and hopefully without
|
|
too much distortion in the audio output). */
|
|
do {
|
|
attempt++;
|
|
memmove (&hbuf[0], &hbuf[1], 7);
|
|
if (readFromStream(&hbuf[3],1) != 1) {
|
|
return False;
|
|
}
|
|
|
|
/* This is faster than combining fr().hdr from scratch */
|
|
fr().hdr = ((fr().hdr << 8) | hbuf[3]) & 0xffffffff;
|
|
|
|
if (!fr().oldHdr)
|
|
goto init_resync; /* "considered harmful", eh? */
|
|
|
|
} while ((fr().hdr & HDRCMPMASK) != (fr().oldHdr & HDRCMPMASK)
|
|
&& (fr().hdr & HDRCMPMASK) != (fr().firstHdr & HDRCMPMASK));
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf (stderr, "Skipped %d bytes in input.\n", attempt);
|
|
#endif
|
|
}
|
|
if (!fr().firstHdr) {
|
|
fr().firstHdr = fr().hdr;
|
|
}
|
|
|
|
fr().setParamsFromHeader();
|
|
fr().setBytePointer(fr().frameBytes, fr().frameSize);
|
|
|
|
fr().oldHdr = fr().hdr;
|
|
|
|
if (fr().isFreeFormat) {
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr,"Free format not supported.\n");
|
|
#endif
|
|
return False;
|
|
}
|
|
|
|
#ifdef MP3_ONLY
|
|
if (fr().layer != 3) {
|
|
#ifdef DEBUG_ERRORS
|
|
fprintf(stderr, "MPEG layer %d is not supported!\n", fr().layer);
|
|
#endif
|
|
return False;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if ((l = readFromStream(fr().frameBytes, fr().frameSize))
|
|
!= fr().frameSize) {
|
|
if (l == 0) return False;
|
|
memset(fr().frameBytes+1, 0, fr().frameSize-1);
|
|
}
|
|
|
|
return True;
|
|
}
|
|
|
|
static Boolean socketIsReadable(int socket) {
|
|
const unsigned numFds = socket+1;
|
|
fd_set rd_set;
|
|
FD_ZERO(&rd_set);
|
|
FD_SET((unsigned)socket, &rd_set);
|
|
struct timeval timeout;
|
|
timeout.tv_sec = timeout.tv_usec = 0;
|
|
|
|
int result = select(numFds, &rd_set, NULL, NULL, &timeout);
|
|
return result != 0; // not > 0, because windows can return -1 for file sockets
|
|
}
|
|
|
|
static char watchVariable;
|
|
|
|
static void checkFunc(void* /*clientData*/) {
|
|
watchVariable = ~0;
|
|
}
|
|
|
|
static void waitUntilSocketIsReadable(UsageEnvironment& env, int socket) {
|
|
while (!socketIsReadable(socket)) {
|
|
// Delay a short period of time before checking again.
|
|
unsigned usecsToDelay = 1000; // 1 ms
|
|
env.taskScheduler().scheduleDelayedTask(usecsToDelay,
|
|
(TaskFunc*)checkFunc, (void*)NULL);
|
|
watchVariable = 0;
|
|
env.taskScheduler().doEventLoop(&watchVariable);
|
|
// This allows other tasks to run while we're waiting:
|
|
}
|
|
}
|
|
|
|
unsigned MP3StreamState::readFromStream(unsigned char* buf,
|
|
unsigned numChars) {
|
|
// Hack for doing socket I/O instead of file I/O (e.g., on Windows)
|
|
if (fFidIsReallyASocket) {
|
|
intptr_t fid_long = (intptr_t)fFid;
|
|
int sock = (int)fid_long;
|
|
unsigned totBytesRead = 0;
|
|
do {
|
|
waitUntilSocketIsReadable(fEnv, sock);
|
|
int bytesRead
|
|
= recv(sock, &((char*)buf)[totBytesRead], numChars-totBytesRead, 0);
|
|
if (bytesRead < 0) return 0;
|
|
|
|
totBytesRead += (unsigned)bytesRead;
|
|
} while (totBytesRead < numChars);
|
|
|
|
return totBytesRead;
|
|
} else {
|
|
#ifndef _WIN32_WCE
|
|
waitUntilSocketIsReadable(fEnv, (int)fileno(fFid));
|
|
#endif
|
|
return fread(buf, 1, numChars, fFid);
|
|
}
|
|
}
|
|
|
|
#define XING_FRAMES_FLAG 0x0001
|
|
#define XING_BYTES_FLAG 0x0002
|
|
#define XING_TOC_FLAG 0x0004
|
|
#define XING_VBR_SCALE_FLAG 0x0008
|
|
|
|
void MP3StreamState::checkForXingHeader() {
|
|
// Look for 'Xing' in the first 4 bytes after the 'side info':
|
|
if (fr().frameSize < fr().sideInfoSize) return;
|
|
unsigned bytesAvailable = fr().frameSize - fr().sideInfoSize;
|
|
unsigned char* p = &(fr().frameBytes[fr().sideInfoSize]);
|
|
|
|
if (bytesAvailable < 8) return;
|
|
if (p[0] != 'X' || p[1] != 'i' || p[2] != 'n' || p[3] != 'g') return;
|
|
|
|
// We found it.
|
|
fIsVBR = True;
|
|
|
|
u_int32_t flags = (p[4]<<24) | (p[5]<<16) | (p[6]<<8) | p[7];
|
|
unsigned i = 8;
|
|
bytesAvailable -= 8;
|
|
|
|
if (flags&XING_FRAMES_FLAG) {
|
|
// The next 4 bytes are the number of frames:
|
|
if (bytesAvailable < 4) return;
|
|
fNumFramesInFile = (p[i]<<24)|(p[i+1]<<16)|(p[i+2]<<8)|(p[i+3]);
|
|
i += 4; bytesAvailable -= 4;
|
|
}
|
|
|
|
if (flags&XING_BYTES_FLAG) {
|
|
// The next 4 bytes is the file size:
|
|
if (bytesAvailable < 4) return;
|
|
fFileSize = (p[i]<<24)|(p[i+1]<<16)|(p[i+2]<<8)|(p[i+3]);
|
|
i += 4; bytesAvailable -= 4;
|
|
}
|
|
|
|
if (flags&XING_TOC_FLAG) {
|
|
// Fill in the Xing 'table of contents':
|
|
if (bytesAvailable < XING_TOC_LENGTH) return;
|
|
fHasXingTOC = True;
|
|
for (unsigned j = 0; j < XING_TOC_LENGTH; ++j) {
|
|
fXingTOC[j] = p[i+j];
|
|
}
|
|
i += XING_TOC_FLAG; bytesAvailable -= XING_TOC_FLAG;
|
|
}
|
|
}
|