ciderpress/util/SoundFile.cpp
2007-03-27 17:47:10 +00:00

229 lines
5.8 KiB
C++

/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Read-only encapsulation of a sound file.
*
* Microsoft supplies a number of functions (e.g. mmioOpen) that do a lot of
* this, but it's built on top of fairly complex machinery designed to
* support AVI files and read/write WAV files. It actually requires about
* the same amount of code to parse a WAV file without resorting to MMIO.
*/
#include "StdAfx.h"
#include "SoundFile.h"
/*
* Convert four characters into a single 4-byte constant.
*
* The constant is expected to match 4-character values read from a binary
* file, so the value is endian-dependent.
*/
static inline unsigned long
MakeFourCC(unsigned char c0, unsigned char c1, unsigned char c2,
unsigned char c3)
{
/* little-endian */
return ((unsigned long)c0) | ((unsigned long)c1 << 8) |
((unsigned long)c2 << 16) | ((unsigned long)c3 << 24);
}
/*
* Create the object from a file on disk.
*
* Returns 0 on success.
*/
int
SoundFile::Create(const char* fileName, CString* pErrMsg)
{
FILE* fp = nil;
long fileLen;
fp = fopen(fileName, "rb");
if (fp == nil) {
int err = errno;
pErrMsg->Format("Unable to open '%s'", fileName);
return err;
}
fseek(fp, 0, SEEK_END);
fileLen = ftell(fp);
rewind(fp);
return Create(fp, fileLen, true, pErrMsg);
}
/*
* Create the object from an already-open file pointer. The file must be
* seeked to the start of the WAV file.
*
* Returns 0 on success.
*/
int
SoundFile::Create(FILE* fp, long len, bool doClose, CString* pErrMsg)
{
struct {
unsigned long riff;
unsigned long fileLen;
unsigned long wav;
} fileHeader;
unsigned long chunkLen;
int err = 0;
if (mFP != nil) {
WMSG0("SoundFile object already created\n");
assert(false);
return -1;
}
if (fp == nil)
return -1;
if (len < kWAVMinSize) {
*pErrMsg = "File is too short to be WAV";
return -1;
}
/* from here on out, we own fp if doClose==true */
mFP = fp;
mDoClose = doClose;
mFileStart = ftell(mFP); // so we can rewind if necessary
/*
* Check the first header.
*/
if (fread(&fileHeader, sizeof(fileHeader), 1, mFP) != 1) {
err = errno ? errno : -1;
*pErrMsg = "Failed reading file header";
goto bail;
}
if (fileHeader.riff != MakeFourCC('R','I','F','F') ||
fileHeader.wav != MakeFourCC('W','A','V','E') ||
fileHeader.fileLen > (unsigned long) len)
{
*pErrMsg = "File is not a WAV file";
WMSG3("Not a valid WAV header (0x%08lx %d 0x%08lx)\n",
fileHeader.riff, fileHeader.fileLen, fileHeader.wav);
err = -1;
goto bail;
}
/*
* Find the "fmt " chunk, which holds the WAV format data. This should
* immediately follow the file header, but I'm not sure that's mandatory.
* I *am* going to assume that it precedes the "data" chunk, so I don't
* have to rewind the file after finding "fmt ".
*/
err = SkipToHeader(MakeFourCC('f','m','t',' '), &chunkLen);
if (err != 0)
goto bail;
/*
* Read the WAVEFORMATEX structure directly from the file. This is useful
* as an argument to certain DirectX functions. The structure contains
* an extra field, so we need to zero it out and use chunkLen rather
* than the structure size.
*/
if (chunkLen > sizeof(WAVEFORMATEX)) {
pErrMsg->Format("Bad WAV file: 'fmt ' size is %d, struct is %d",
chunkLen, sizeof(WAVEFORMATEX));
err = -1;
goto bail;
}
//memset(&mFormat, 0, sizeof(mFormat)); // done in constructor
if (fread(&mFormat, chunkLen, 1, mFP) != 1) {
err = errno ? errno : -1;
*pErrMsg = "Failed reading WAVEFORMATEX";
goto bail;
}
/* check the format for compatibility */
if (mFormat.wFormatTag != WAVE_FORMAT_PCM) {
*pErrMsg = "WAV file is not PCM format";
err = -1;
goto bail;
}
/*
* Find the "data" chunk, which holds the actual sound samples.
*/
err = SkipToHeader(MakeFourCC('d','a','t','a'), &chunkLen);
if (err != 0)
goto bail;
mSampleStart = ftell(mFP);
mSampleLen = chunkLen;
WMSG4("WAV: chan=%d samples/sec=%d avgBPS=%d block=%d\n",
mFormat.nChannels, mFormat.nSamplesPerSec, mFormat.nAvgBytesPerSec,
mFormat.nBlockAlign);
WMSG3(" bits/sample=%d [start=%d len=%d]\n", mFormat.wBitsPerSample,
mSampleStart, mSampleLen);
bail:
return err;
}
/*
* Skip forward until we find the named chunk. The file should be
* positioned immediately before the first chunk.
*/
int
SoundFile::SkipToHeader(unsigned long hdrID, unsigned long* pChunkLen)
{
struct {
unsigned long fourcc;
unsigned long chunkLen;
} chunkHeader;
int err = 0;
/* loop until we find the right chunk or run out of file */
while (true) {
if (fread(&chunkHeader, sizeof(chunkHeader), 1, mFP) != 1) {
err = errno ? errno : -1;
WMSG1("Failed searching for chunk header 0x%08lx\n", hdrID);
break;
}
if (chunkHeader.fourcc == hdrID) {
*pChunkLen = chunkHeader.chunkLen;
break;
}
/* didn't match, skip contents */
if (fseek(mFP, chunkHeader.chunkLen, SEEK_CUR) != 0) {
err = errno;
WMSG1("Failed seeking past contents of 0x%08lx\n",
chunkHeader.chunkLen);
break;
}
}
return err;
}
/*
* Read a block of data from the specified offset.
*/
int
SoundFile::ReadData(void* buf, long sampleOffset, long len) const
{
if ((unsigned long)(sampleOffset+len) > mSampleLen) {
WMSG3("ERROR: invalid read request (%d bytes, %d into %d)\n",
len, sampleOffset, mSampleLen);
return -1;
}
if (fseek(mFP, mSampleStart+sampleOffset, SEEK_SET) != 0)
return errno;
if (fread(buf, len, 1, mFP) != 1) {
int err = errno ? errno : -1;
WMSG1("Failed reading %d bytes from sound file\n", len);
return err;
}
return 0;
}