audacia/src/BlockFile.cpp

772 lines
25 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
BlockFile.cpp
Joshua Haberman
Dominic Mazzoni
*******************************************************************//*!
\class BlockFile
\brief A BlockFile is a chunk of immutable audio data.
A BlockFile represents a chunk of audio data. These chunks are
assembled into sequences by the class Sequence. These classes
are at the heart of how Audacity stores audio data.
BlockFile is an abstract base class that can be implemented in
many different ways. However it does have a fairly large amount
of shared code that deals with the physical file and manipulating
the summary data.
BlockFile should be thought of as an immutable class. After it
is constructed, it is essentially never changed (though there are
a few exceptions). Most notably, the audio data and summary data
are never altered once it is constructed. This is important to
some of the derived classes that are actually aliases to audio
data stored in existing files.
BlockFiles are reference-counted, and deleted when their reference
count hits zero. DirManager is the class responsible for
constructing and managing BlockFiles and managing their reference
counts.
*//****************************************************************//**
\class SummaryInfo
\brief Works with BlockFile to hold info about max and min and RMS
over multiple samples, which in turn allows rapid drawing when zoomed
out.
*//*******************************************************************/
#include "Audacity.h"
#include "BlockFile.h"
#include <float.h>
#include <cmath>
#include <wx/utils.h>
#include <wx/filefn.h>
#include <wx/ffile.h>
#include <wx/log.h>
#include "Internat.h"
#include "MemoryX.h"
#include "sndfile.h"
#include "FileFormats.h"
#include "AudacityApp.h"
// msmeyer: Define this to add debug output via printf()
//#define DEBUG_BLOCKFILE
#ifdef DEBUG_BLOCKFILE
#define BLOCKFILE_DEBUG_OUTPUT(op, i) \
wxPrintf(wxT("[BlockFile %x %s] %s: %i\n"), (unsigned)this, \
mFileName.GetFullName().c_str(), wxT(op), i);
#else
#define BLOCKFILE_DEBUG_OUTPUT(op, i)
#endif
static const int headerTagLen = 20;
static char headerTag[headerTagLen + 1] = "AudacityBlockFile112";
SummaryInfo::SummaryInfo(size_t samples)
{
format = floatSample;
fields = 3; /* min, max, rms */
bytesPerFrame = sizeof(float) * fields;
frames64K = (samples + 65535) / 65536;
frames256 = frames64K * 256;
offset64K = headerTagLen;
offset256 = offset64K + (frames64K * bytesPerFrame);
totalSummaryBytes = offset256 + (frames256 * bytesPerFrame);
}
ArrayOf<char> BlockFile::fullSummary;
/// Initializes the base BlockFile data. The block is initially
/// unlocked and its reference count is 1.
///
/// @param fileName The name of the disk file associated with this
/// BlockFile. Not all BlockFiles will store their
/// sample data here (for example, AliasBlockFiles
/// read their data from elsewhere), but all BlockFiles
/// will store at least the summary data here.
///
/// @param samples The number of samples this BlockFile contains.
BlockFile::BlockFile(wxFileNameWrapper &&fileName, size_t samples):
mLockCount(0),
mFileName(std::move(fileName)),
mLen(samples),
mSummaryInfo(samples)
{
mSilentLog=FALSE;
}
// static
unsigned long BlockFile::gBlockFileDestructionCount { 0 };
BlockFile::~BlockFile()
{
if (!IsLocked() && mFileName.HasName())
wxRemoveFile(mFileName.GetFullPath());
++gBlockFileDestructionCount;
}
/// Returns the file name of the disk file associated with this
/// BlockFile. Not all BlockFiles store their sample data here,
/// but most BlockFiles have at least their summary data here.
/// (some, i.e. SilentBlockFiles, do not correspond to a file on
/// disk and have empty file names)
auto BlockFile::GetFileName() const -> GetFileNameResult
{
return { mFileName };
}
///sets the file name the summary info will be saved in. threadsafe.
void BlockFile::SetFileName(wxFileNameWrapper &&name)
{
mFileName=std::move(name);
}
/// Marks this BlockFile as "locked." A locked BlockFile may not
/// be moved or deleted, only copied. Locking a BlockFile prevents
/// it from disappearing if the project is saved in a different location.
/// When doing a "Save As," Audacity locks all blocks belonging
/// to the already-existing project, to ensure that the existing
/// project remains valid with all the blocks it needs. Audacity
/// also locks the blocks of the last saved version of a project when
/// the project is deleted so that the files aren't deleted when their
/// refcount hits zero.
void BlockFile::Lock()
{
mLockCount++;
BLOCKFILE_DEBUG_OUTPUT("Lock", mLockCount);
}
/// Marks this BlockFile as "unlocked."
void BlockFile::Unlock()
{
mLockCount--;
BLOCKFILE_DEBUG_OUTPUT("Unlock", mLockCount);
}
/// Returns true if the block is locked.
bool BlockFile::IsLocked()
{
return mLockCount > 0;
}
/// Get a buffer containing a summary block describing this sample
/// data. This must be called by derived classes when they
/// are constructed, to allow them to construct their summary data,
/// after which they should write that data to their disk file.
///
/// This method also has the side effect of setting the mMin, mMax,
/// and mRMS members of this class.
///
/// You must not DELETE the returned buffer; it is static to this
/// method.
///
/// @param buffer A buffer containing the sample data to be analyzed
/// @param len The length of the sample data
/// @param format The format of the sample data.
void *BlockFile::CalcSummary(samplePtr buffer, size_t len,
sampleFormat format, ArrayOf<char> &cleanup)
{
// Caller has nothing to deallocate
cleanup.reset();
fullSummary.reinit(mSummaryInfo.totalSummaryBytes);
memcpy(fullSummary.get(), headerTag, headerTagLen);
float *summary64K = (float *)(fullSummary.get() + mSummaryInfo.offset64K);
float *summary256 = (float *)(fullSummary.get() + mSummaryInfo.offset256);
float *fbuffer = new float[len];
CopySamples(buffer, format,
(samplePtr)fbuffer, floatSample, len);
CalcSummaryFromBuffer(fbuffer, len, summary256, summary64K);
delete[] fbuffer;
return fullSummary.get();
}
void BlockFile::CalcSummaryFromBuffer(const float *fbuffer, size_t len,
float *summary256, float *summary64K)
{
decltype(len) sumLen;
float min, max;
float sumsq;
double totalSquares = 0.0;
double fraction { 0.0 };
// Recalc 256 summaries
sumLen = (len + 255) / 256;
int summaries = 256;
for (decltype(sumLen) i = 0; i < sumLen; i++) {
min = fbuffer[i * 256];
max = fbuffer[i * 256];
sumsq = ((float)min) * ((float)min);
decltype(len) jcount = 256;
if (jcount > len - i * 256) {
jcount = len - i * 256;
fraction = 1.0 - (jcount / 256.0);
}
for (decltype(jcount) j = 1; j < jcount; j++) {
float f1 = fbuffer[i * 256 + j];
sumsq += ((float)f1) * ((float)f1);
if (f1 < min)
min = f1;
else if (f1 > max)
max = f1;
}
totalSquares += sumsq;
float rms = (float)sqrt(sumsq / jcount);
summary256[i * 3] = min;
summary256[i * 3 + 1] = max;
summary256[i * 3 + 2] = rms; // The rms is correct, but this may be for less than 256 samples in last loop.
}
for (auto i = sumLen; i < mSummaryInfo.frames256; i++) {
// filling in the remaining bits with non-harming/contributing values
// rms values are not "non-harming", so keep count of them:
summaries--;
summary256[i * 3] = FLT_MAX; // min
summary256[i * 3 + 1] = -FLT_MAX; // max
summary256[i * 3 + 2] = 0.0f; // rms
}
// Calculate now while we can do it accurately
mRMS = sqrt(totalSquares/len);
// Recalc 64K summaries
sumLen = (len + 65535) / 65536;
for (decltype(sumLen) i = 0; i < sumLen; i++) {
min = summary256[3 * i * 256];
max = summary256[3 * i * 256 + 1];
sumsq = (float)summary256[3 * i * 256 + 2];
sumsq *= sumsq;
for (decltype(len) j = 1; j < 256; j++) { // we can overflow the useful summary256 values here, but have put non-harmful values in them
if (summary256[3 * (i * 256 + j)] < min)
min = summary256[3 * (i * 256 + j)];
if (summary256[3 * (i * 256 + j) + 1] > max)
max = summary256[3 * (i * 256 + j) + 1];
float r1 = summary256[3 * (i * 256 + j) + 2];
sumsq += r1*r1;
}
double denom = (i < sumLen - 1) ? 256.0 : summaries - fraction;
float rms = (float)sqrt(sumsq / denom);
summary64K[i * 3] = min;
summary64K[i * 3 + 1] = max;
summary64K[i * 3 + 2] = rms;
}
for (auto i = sumLen; i < mSummaryInfo.frames64K; i++) {
wxASSERT_MSG(false, wxT("Out of data for mSummaryInfo")); // Do we ever get here?
summary64K[i * 3] = 0.0f; // probably should be FLT_MAX, need a test case
summary64K[i * 3 + 1] = 0.0f; // probably should be -FLT_MAX, need a test case
summary64K[i * 3 + 2] = 0.0f; // just padding
}
// Recalc block-level summary (mRMS already calculated)
min = summary64K[0];
max = summary64K[1];
for (decltype(sumLen) i = 1; i < sumLen; i++) {
if (summary64K[3*i] < min)
min = summary64K[3*i];
if (summary64K[3*i+1] > max)
max = summary64K[3*i+1];
}
mMin = min;
mMax = max;
}
static void ComputeMinMax256(float *summary256,
float *outMin, float *outMax, int *outBads)
{
float min, max;
int i;
int bad = 0;
min = 1.0;
max = -1.0;
for(i=0; i<256; i++) {
if (summary256[3*i] < min)
min = summary256[3*i];
else if (!(summary256[3*i] >= min))
bad++;
if (summary256[3*i+1] > max)
max = summary256[3*i+1];
else if (!(summary256[3*i+1] <= max))
bad++;
if (std::isnan(summary256[3*i+2]))
bad++;
if (summary256[3*i+2] < -1 || summary256[3*i+2] > 1)
bad++;
}
*outMin = min;
*outMax = max;
*outBads = bad;
}
/// Byte-swap the summary data, in case it was saved by a system
/// on a different platform
void BlockFile::FixSummary(void *data)
{
if (mSummaryInfo.format != floatSample ||
mSummaryInfo.fields != 3)
return;
float *summary64K = (float *)((char *)data + mSummaryInfo.offset64K);
float *summary256 = (float *)((char *)data + mSummaryInfo.offset256);
float min, max;
int bad;
int i;
ComputeMinMax256(summary256, &min, &max, &bad);
if (min != summary64K[0] || max != summary64K[1] || bad > 0) {
unsigned int *buffer = (unsigned int *)data;
auto len = mSummaryInfo.totalSummaryBytes / 4;
for(i=0; i<len; i++)
buffer[i] = wxUINT32_SWAP_ALWAYS(buffer[i]);
ComputeMinMax256(summary256, &min, &max, &bad);
if (min == summary64K[0] && max == summary64K[1] && bad == 0) {
// The byte-swapping worked!
return;
}
// Hmmm, no better, we should swap back
for(i=0; i<len; i++)
buffer[i] = wxUINT32_SWAP_ALWAYS(buffer[i]);
}
}
/// Retrieves the minimum, maximum, and maximum RMS of the
/// specified sample data in this block.
///
/// @param start The offset in this block where the region should begin
/// @param len The number of samples to include in the region
/// @param *outMin A pointer to where the minimum value for this region
/// should be stored
/// @param *outMax A pointer to where the maximum value for this region
/// should be stored
/// @param *outRMS A pointer to where the maximum RMS value for this
/// region should be stored.
void BlockFile::GetMinMax(size_t start, size_t len,
float *outMin, float *outMax, float *outRMS) const
{
// TODO: actually use summaries
SampleBuffer blockData(len, floatSample);
this->ReadData(blockData.ptr(), floatSample, start, len);
float min = FLT_MAX;
float max = -FLT_MAX;
float sumsq = 0;
for( decltype(len) i = 0; i < len; i++ )
{
float sample = ((float*)blockData.ptr())[i];
if( sample > max )
max = sample;
if( sample < min )
min = sample;
sumsq += (sample*sample);
}
*outMin = min;
*outMax = max;
*outRMS = sqrt(sumsq/len);
}
/// Retrieves the minimum, maximum, and maximum RMS of this entire
/// block. This is faster than the other GetMinMax function since
/// these values are already computed.
///
/// @param *outMin A pointer to where the minimum value for this block
/// should be stored
/// @param *outMax A pointer to where the maximum value for this block
/// should be stored
/// @param *outRMS A pointer to where the maximum RMS value for this
/// block should be stored.
void BlockFile::GetMinMax(float *outMin, float *outMax, float *outRMS) const
{
*outMin = mMin;
*outMax = mMax;
*outRMS = mRMS;
}
/// Retrieves a portion of the 256-byte summary buffer from this BlockFile. This
/// data provides information about the minimum value, the maximum
/// value, and the maximum RMS value for every group of 256 samples in the
/// file.
///
/// @param *buffer The area where the summary information will be
/// written. It must be at least len*3 long.
/// @param start The offset in 256-sample increments
/// @param len The number of 256-sample summary frames to read
bool BlockFile::Read256(float *buffer,
size_t start, size_t len)
{
wxASSERT(start >= 0);
char *summary = new char[mSummaryInfo.totalSummaryBytes];
// FIXME: TRAP_ERR ReadSummary() could return fail.
this->ReadSummary(summary);
start = std::min( start, mSummaryInfo.frames256 );
len = std::min( len, mSummaryInfo.frames256 - start );
CopySamples(summary + mSummaryInfo.offset256 + (start * mSummaryInfo.bytesPerFrame),
mSummaryInfo.format,
(samplePtr)buffer, floatSample, len * mSummaryInfo.fields);
if (mSummaryInfo.fields == 2) {
// No RMS info
for(auto i = len; i--;) {
buffer[3*i+2] = (fabs(buffer[2*i]) + fabs(buffer[2*i+1]))/4.0;
buffer[3*i+1] = buffer[2*i+1];
buffer[3*i] = buffer[2*i];
}
}
delete[] summary;
return true;
}
/// Retrieves a portion of the 64K summary buffer from this BlockFile. This
/// data provides information about the minimum value, the maximum
/// value, and the maximum RMS value for every group of 64K samples in the
/// file.
///
/// @param *buffer The area where the summary information will be
/// written. It must be at least len*3 long.
/// @param start The offset in 64K-sample increments
/// @param len The number of 64K-sample summary frames to read
bool BlockFile::Read64K(float *buffer,
size_t start, size_t len)
{
wxASSERT(start >= 0);
char *summary = new char[mSummaryInfo.totalSummaryBytes];
// FIXME: TRAP_ERR ReadSummary() could return fail.
this->ReadSummary(summary);
start = std::min( start, mSummaryInfo.frames64K );
len = std::min( len, mSummaryInfo.frames64K - start );
CopySamples(summary + mSummaryInfo.offset64K +
(start * mSummaryInfo.bytesPerFrame),
mSummaryInfo.format,
(samplePtr)buffer, floatSample, len*mSummaryInfo.fields);
if (mSummaryInfo.fields == 2) {
// No RMS info; make guess
for(auto i = len; i--;) {
buffer[3*i+2] = (fabs(buffer[2*i]) + fabs(buffer[2*i+1]))/4.0;
buffer[3*i+1] = buffer[2*i+1];
buffer[3*i] = buffer[2*i];
}
}
delete[] summary;
return true;
}
size_t BlockFile::CommonReadData(
const wxFileName &fileName, bool &mSilentLog,
const AliasBlockFile *pAliasFile, sampleCount origin, unsigned channel,
samplePtr data, sampleFormat format, size_t start, size_t len,
const sampleFormat *pLegacyFormat, size_t legacyLen)
{
// Third party library has its own type alias, check it before
// adding origin + size_t
static_assert(sizeof(sampleCount::type) <= sizeof(sf_count_t),
"Type sf_count_t is too narrow to hold a sampleCount");
SF_INFO info;
memset(&info, 0, sizeof(info));
if ( pLegacyFormat ) {
switch( *pLegacyFormat ) {
case int16Sample:
info.format =
SF_FORMAT_RAW | SF_FORMAT_PCM_16 | SF_ENDIAN_CPU;
break;
default:
case floatSample:
info.format =
SF_FORMAT_RAW | SF_FORMAT_FLOAT | SF_ENDIAN_CPU;
break;
case int24Sample:
info.format = SF_FORMAT_RAW | SF_FORMAT_PCM_32 | SF_ENDIAN_CPU;
break;
}
info.samplerate = 44100; // Doesn't matter
info.channels = 1;
info.frames = legacyLen + origin.as_long_long();
}
wxFile f; // will be closed when it goes out of scope
SFFile sf;
{
Maybe<wxLogNull> silence{};
if (mSilentLog)
silence.create();
const auto fullPath = fileName.GetFullPath();
if (wxFile::Exists(fullPath) && f.Open(fullPath)) {
// Even though there is an sf_open() that takes a filename, use the one that
// takes a file descriptor since wxWidgets can open a file with a Unicode name and
// libsndfile can't (under Windows).
sf.reset(SFCall<SNDFILE*>(sf_open_fd, f.fd(), SFM_READ, &info, FALSE));
}
// FIXME: TRAP_ERR failure of wxFile open incompletely handled in BlockFile::CommonReadData.
if (!sf) {
memset(data, 0, SAMPLE_SIZE(format)*len);
mSilentLog = TRUE;
if (pAliasFile) {
// Set a marker to display an error message for the silence
if (!wxGetApp().ShouldShowMissingAliasedFileWarning())
wxGetApp().MarkAliasedFilesMissingWarning(pAliasFile);
}
return len;
}
}
mSilentLog=FALSE;
SFCall<sf_count_t>(
sf_seek, sf.get(), ( origin + start ).as_long_long(), SEEK_SET);
auto channels = info.channels;
wxASSERT(channels >= 1);
wxASSERT(channel < channels);
size_t framesRead = 0;
if (channels == 1 &&
format == int16Sample &&
sf_subtype_is_integer(info.format)) {
// If both the src and dest formats are integer formats,
// read integers directly from the file, comversions not needed
framesRead = SFCall<sf_count_t>(
sf_readf_short, sf.get(), (short *)data, len);
}
else if (channels == 1 &&
format == int24Sample &&
sf_subtype_is_integer(info.format)) {
framesRead = SFCall<sf_count_t>(sf_readf_int, sf.get(), (int *)data, len);
// libsndfile gave us the 3 byte sample in the 3 most
// significant bytes -- we want it in the 3 least
// significant bytes.
int *intPtr = (int *)data;
for( int i = 0; i < framesRead; i++ )
intPtr[i] = intPtr[i] >> 8;
}
else if (format == int16Sample &&
!sf_subtype_more_than_16_bits(info.format)) {
// Special case: if the file is in 16-bit (or less) format,
// and the calling method wants 16-bit data, go ahead and
// read 16-bit data directly. This is a pretty common
// case, as most audio files are 16-bit.
SampleBuffer buffer(len * channels, int16Sample);
framesRead = SFCall<sf_count_t>(
sf_readf_short, sf.get(), (short *)buffer.ptr(), len);
for (int i = 0; i < framesRead; i++)
((short *)data)[i] =
((short *)buffer.ptr())[(channels * i) + channel];
}
else {
// Otherwise, let libsndfile handle the conversion and
// scaling, and pass us normalized data as floats. We can
// then convert to whatever format we want.
SampleBuffer buffer(len * channels, floatSample);
framesRead = SFCall<sf_count_t>(
sf_readf_float, sf.get(), (float *)buffer.ptr(), len);
auto bufferPtr = (samplePtr)((float *)buffer.ptr() + channel);
CopySamples(bufferPtr, floatSample,
(samplePtr)data, format,
framesRead,
true /* high quality by default */,
channels /* source stride */);
}
return framesRead;
}
/// Constructs an AliasBlockFile based on the given information about
/// the aliased file.
///
/// Note that derived classes /must/ call AliasBlockFile::WriteSummary()
/// in their constructors for the summary file to be correctly written
/// to disk.
///
/// @param baseFileName The name of the summary file to be written, but
/// without an extension. This constructor will add
/// the appropriate extension before passing it to
/// BlockFile::BlockFile
/// @param aliasedFileName The name of the file where the audio data for
/// this block actually exists.
/// @param aliasStart The offset in the aliased file where this block's
/// data begins
/// @param aliasLen The length of this block's data in the aliased
/// file.
/// @param aliasChannel The channel where this block's data is located in
/// the aliased file
AliasBlockFile::AliasBlockFile(wxFileNameWrapper &&baseFileName,
wxFileNameWrapper &&aliasedFileName,
sampleCount aliasStart,
size_t aliasLen, int aliasChannel):
BlockFile {
(baseFileName.SetExt(wxT("auf")), std::move(baseFileName)),
aliasLen
},
mAliasedFileName(std::move(aliasedFileName)),
mAliasStart(aliasStart),
mAliasChannel(aliasChannel)
{
mSilentAliasLog=FALSE;
}
AliasBlockFile::AliasBlockFile(wxFileNameWrapper &&existingSummaryFileName,
wxFileNameWrapper &&aliasedFileName,
sampleCount aliasStart,
size_t aliasLen,
int aliasChannel,
float min, float max, float rms):
BlockFile{ std::move(existingSummaryFileName), aliasLen },
mAliasedFileName(std::move(aliasedFileName)),
mAliasStart(aliasStart),
mAliasChannel(aliasChannel)
{
mMin = min;
mMax = max;
mRMS = rms;
mSilentAliasLog=FALSE;
}
/// Write the summary to disk. Derived classes must call this method
/// from their constructors for the summary to be correctly written.
/// It uses the derived class's ReadData() to retrieve the data to
/// summarize.
void AliasBlockFile::WriteSummary()
{
// Now checked carefully in the DirManager
//wxASSERT( !wxFileExists(FILENAME(mFileName.GetFullPath())));
// I would much rather have this code as part of the constructor, but
// I can't call virtual functions from the constructor. So we just
// need to ensure that every derived class calls this in *its* constructor
wxFFile summaryFile(mFileName.GetFullPath(), wxT("wb"));
if( !summaryFile.IsOpened() ){
// Never silence the Log w.r.t write errors; they always count
// as NEW errors
wxLogError(wxT("Unable to write summary data to file %s"),
mFileName.GetFullPath().c_str());
// If we can't write, there's nothing to do.
return;
}
// To build the summary data, call ReadData (implemented by the
// derived classes) to get the sample data
SampleBuffer sampleData(mLen, floatSample);
this->ReadData(sampleData.ptr(), floatSample, 0, mLen);
ArrayOf<char> cleanup;
void *summaryData = BlockFile::CalcSummary(sampleData.ptr(), mLen,
floatSample, cleanup);
summaryFile.Write(summaryData, mSummaryInfo.totalSummaryBytes);
}
AliasBlockFile::~AliasBlockFile()
{
}
/// Read the summary of this alias block from disk. Since the audio data
/// is elsewhere, this consists of reading the entire summary file.
///
/// @param *data The buffer where the summary data will be stored. It must
/// be at least mSummaryInfo.totalSummaryBytes long.
bool AliasBlockFile::ReadSummary(void *data)
{
wxFFile summaryFile(mFileName.GetFullPath(), wxT("rb"));
{
Maybe<wxLogNull> silence{};
if (mSilentLog)
silence.create();
if (!summaryFile.IsOpened()){
// NEW model; we need to return valid data
memset(data, 0, mSummaryInfo.totalSummaryBytes);
// we silence the logging for this operation in this object
// after first occurrence of error; it's already reported and
// spewing at the user will complicate the user's ability to
// deal
mSilentLog = TRUE;
return true;
}
else mSilentLog = FALSE; // worked properly, any future error is NEW
}
auto read = summaryFile.Read(data, mSummaryInfo.totalSummaryBytes);
FixSummary(data);
return (read == mSummaryInfo.totalSummaryBytes);
}
/// Modify this block to point at a different file. This is generally
/// looked down on, but it is necessary in one case: see
/// DirManager::EnsureSafeFilename().
void AliasBlockFile::ChangeAliasedFileName(wxFileNameWrapper &&newAliasedFile)
{
mAliasedFileName = std::move(newAliasedFile);
}
auto AliasBlockFile::GetSpaceUsage() const -> DiskByteCount
{
wxFFile summaryFile(mFileName.GetFullPath());
return summaryFile.Length();
}