audacia/src/blockfile/SimpleBlockFile.cpp

613 lines
18 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
SimpleBlockFile.cpp
Joshua Haberman
Markus Meyer
*******************************************************************//**
\file SimpleBlockFile.cpp
\brief Implements SimpleBlockFile and auHeader.
*//****************************************************************//**
\class SimpleBlockFile
\brief A BlockFile that reads and writes uncompressed data using
libsndfile
A block file that writes the audio data to an .au file and reads
it back using libsndfile.
There are two ways to construct a simple block file. One is to
supply data and have the constructor write the file. The other
is for when the file already exists and we simply want to create
the data structure to refer to it.
The block file can be cached in two ways. Caching is enabled if the
preference "/Directories/CacheBlockFiles" is set, otherwise disabled. The
default is to disable caching.
* Read-caching: If caching is enabled, all block files will always be
read-cached. Block files on disk will be read as soon as they are created
and held in memory. New block files will be written to disk, but held in
memory, so they are never read from disk in the current session.
* Write-caching: If caching is enabled and the parameter allowDeferredWrite
is enabled at the block file constructor, NEW block files are held in memory
and written to disk only when WriteCacheToDisk() is called. This is used
during recording to prevent disk access. After recording, WriteCacheToDisk()
will be called on all block files and they will be written to disk. During
normal editing, no write cache is active, that is, any block files will be
written to disk instantly.
Even with write cache, auto recovery during normal editing will work as
expected. However, auto recovery during recording will not work (not even
manual auto recovery, because the files are never written physically to
disk).
*//****************************************************************//**
\class auHeader
\brief The auHeader is a structure used by SimpleBlockFile for .au file
format. There probably is an 'official' header file we should include
to get its definition, rather than rolling our own.
*//*******************************************************************/
#include "../Audacity.h"
#include "SimpleBlockFile.h"
#include <wx/wx.h>
#include <wx/filefn.h>
#include <wx/ffile.h>
#include <wx/utils.h>
#include <wx/log.h>
#include "../DirManager.h"
#include "../Prefs.h"
#include "../FileFormats.h"
#include "sndfile.h"
static wxUint32 SwapUintEndianess(wxUint32 in)
{
wxUint32 out;
unsigned char *p_in = (unsigned char *) &in;
unsigned char *p_out = (unsigned char *) &out;
p_out[0] = p_in[3];
p_out[1] = p_in[2];
p_out[2] = p_in[1];
p_out[3] = p_in[0];
return out;
}
/// Constructs a SimpleBlockFile based on sample data and writes
/// it to disk.
///
/// @param baseFileName The filename to use, but without an extension.
/// This constructor will add the appropriate
/// extension (.au in this case).
/// @param sampleData The sample data to be written to this block.
/// @param sampleLen The number of samples to be written to this block.
/// @param format The format of the given samples.
/// @param allowDeferredWrite Allow deferred write-caching
SimpleBlockFile::SimpleBlockFile(wxFileNameWrapper &&baseFileName,
samplePtr sampleData, size_t sampleLen,
sampleFormat format,
bool allowDeferredWrite /* = false */,
bool bypassCache /* = false */):
BlockFile {
(baseFileName.SetExt(wxT("au")), std::move(baseFileName)),
sampleLen
}
{
mFormat = format;
mCache.active = false;
bool useCache = GetCache() && (!bypassCache);
if (!(allowDeferredWrite && useCache) && !bypassCache)
{
bool bSuccess = WriteSimpleBlockFile(sampleData, sampleLen, format, NULL);
if (!bSuccess)
throw FileException{
FileException::Cause::Write, GetFileName().name };
}
if (useCache) {
//wxLogDebug("SimpleBlockFile::SimpleBlockFile(): Caching block file data.");
mCache.active = true;
mCache.needWrite = true;
mCache.format = format;
const auto sampleDataSize = sampleLen * SAMPLE_SIZE(format);
mCache.sampleData.reinit(sampleDataSize);
memcpy(mCache.sampleData.get(), sampleData, sampleDataSize);
ArrayOf<char> cleanup;
void* summaryData = BlockFile::CalcSummary(sampleData, sampleLen,
format, cleanup);
mCache.summaryData.reinit(mSummaryInfo.totalSummaryBytes);
memcpy(mCache.summaryData.get(), summaryData,
mSummaryInfo.totalSummaryBytes);
}
}
/// Construct a SimpleBlockFile memory structure that will point to an
/// existing block file. This file must exist and be a valid block file.
///
/// @param existingFile The disk file this SimpleBlockFile should use.
SimpleBlockFile::SimpleBlockFile(wxFileNameWrapper &&existingFile, size_t len,
float min, float max, float rms):
BlockFile{ std::move(existingFile), len }
{
// Set an invalid format to force GetSpaceUsage() to read it from the file.
mFormat = (sampleFormat) 0;
mMin = min;
mMax = max;
mRMS = rms;
mCache.active = false;
}
SimpleBlockFile::~SimpleBlockFile()
{
}
bool SimpleBlockFile::WriteSimpleBlockFile(
samplePtr sampleData,
size_t sampleLen,
sampleFormat format,
void* summaryData)
{
wxFFile file(mFileName.GetFullPath(), wxT("wb"));
if( !file.IsOpened() ){
// Can't do anything else.
return false;
}
auHeader header;
// AU files can be either big or little endian. Which it is is
// determined implicitly by the byte-order of the magic 0x2e736e64
// (.snd). We want it to be native-endian, so we write the magic
// to memory and then let it write that to a file in native
// endianness
header.magic = 0x2e736e64;
// We store the summary data at the end of the header, so the data
// offset is the length of the summary data plus the length of the header
header.dataOffset = sizeof(auHeader) + mSummaryInfo.totalSummaryBytes;
// dataSize is optional, and we opt out
header.dataSize = 0xffffffff;
switch(format) {
case int16Sample:
header.encoding = AU_SAMPLE_FORMAT_16;
break;
case int24Sample:
header.encoding = AU_SAMPLE_FORMAT_24;
break;
case floatSample:
header.encoding = AU_SAMPLE_FORMAT_FLOAT;
break;
}
// TODO: don't fabricate
header.sampleRate = 44100;
// BlockFiles are always mono
header.channels = 1;
// Write the file
ArrayOf<char> cleanup;
if (!summaryData)
summaryData = /*BlockFile::*/CalcSummary(sampleData, sampleLen, format, cleanup);
//mchinen:allowing virtual override of calc summary for ODDecodeBlockFile.
// PRL: cleanup fixes a possible memory leak!
size_t nBytesToWrite = sizeof(header);
size_t nBytesWritten = file.Write(&header, nBytesToWrite);
if (nBytesWritten != nBytesToWrite)
{
wxLogDebug(wxT("Wrote %lld bytes, expected %lld."), (long long) nBytesWritten, (long long) nBytesToWrite);
return false;
}
nBytesToWrite = mSummaryInfo.totalSummaryBytes;
nBytesWritten = file.Write(summaryData, nBytesToWrite);
if (nBytesWritten != nBytesToWrite)
{
wxLogDebug(wxT("Wrote %lld bytes, expected %lld."), (long long) nBytesWritten, (long long) nBytesToWrite);
return false;
}
if( format == int24Sample )
{
// we can't write the buffer directly to disk, because 24-bit samples
// on disk need to be packed, not padded to 32 bits like they are in
// memory
int *int24sampleData = (int*)sampleData;
for( size_t i = 0; i < sampleLen; i++ )
{
nBytesToWrite = 3;
nBytesWritten =
#if wxBYTE_ORDER == wxBIG_ENDIAN
file.Write((char*)&int24sampleData[i] + 1, nBytesToWrite);
#else
file.Write((char*)&int24sampleData[i], nBytesToWrite);
#endif
if (nBytesWritten != nBytesToWrite)
{
wxLogDebug(wxT("Wrote %lld bytes, expected %lld."), (long long) nBytesWritten, (long long) nBytesToWrite);
return false;
}
}
}
else
{
// for all other sample formats we can write straight from the buffer
// to disk
nBytesToWrite = sampleLen * SAMPLE_SIZE(format);
nBytesWritten = file.Write(sampleData, nBytesToWrite);
if (nBytesWritten != nBytesToWrite)
{
wxLogDebug(wxT("Wrote %lld bytes, expected %lld."), (long long) nBytesWritten, (long long) nBytesToWrite);
return false;
}
}
return true;
}
// This function should try to fill the cache, but just return without effect
// (not throwing) if there is failure.
void SimpleBlockFile::FillCache()
{
if (mCache.active)
return; // cache is already filled
// Check sample format
wxFFile file(mFileName.GetFullPath(), wxT("rb"));
if (!file.IsOpened())
{
// Don't read into cache if file not available
return;
}
auHeader header;
if (file.Read(&header, sizeof(header)) != sizeof(header))
{
// Corrupt file
return;
}
wxUint32 encoding;
if (header.magic == 0x2e736e64)
encoding = header.encoding; // correct endianness
else
encoding = SwapUintEndianess(header.encoding);
switch (encoding)
{
case AU_SAMPLE_FORMAT_16:
mCache.format = int16Sample;
break;
case AU_SAMPLE_FORMAT_24:
mCache.format = int24Sample;
break;
default:
// floatSample is a safe default (we will never loose data)
mCache.format = floatSample;
break;
}
file.Close();
// Read samples into cache
mCache.sampleData.reinit(mLen * SAMPLE_SIZE(mCache.format));
if (ReadData(mCache.sampleData.get(), mCache.format, 0, mLen,
// no exceptions!
false) != mLen)
{
// Could not read all samples
mCache.sampleData.reset();
return;
}
// Read summary data into cache
// Fills with zeroes in case of failure:
ReadSummary(mCache.summaryData);
// Cache is active but already on disk
mCache.active = true;
mCache.needWrite = false;
//wxLogDebug("SimpleBlockFile::FillCache(): Succesfully read simple block file into cache.");
}
/// Read the summary section of the disk file.
///
/// @param *data The buffer to write the data to. It must be at least
/// mSummaryinfo.totalSummaryBytes long.
bool SimpleBlockFile::ReadSummary(ArrayOf<char> &data)
{
data.reinit( mSummaryInfo.totalSummaryBytes );
if (mCache.active) {
//wxLogDebug("SimpleBlockFile::ReadSummary(): Summary is already in cache.");
memcpy(data.get(), mCache.summaryData.get(), mSummaryInfo.totalSummaryBytes);
return true;
}
else
{
//wxLogDebug("SimpleBlockFile::ReadSummary(): Reading summary from disk.");
wxFFile file(mFileName.GetFullPath(), wxT("rb"));
{
Maybe<wxLogNull> silence{};
if (mSilentLog)
silence.create();
// FIXME: TRAP_ERR no report to user of absent summary files?
// filled with zero instead.
if (!file.IsOpened()){
memset(data.get(), 0, mSummaryInfo.totalSummaryBytes);
mSilentLog = TRUE;
return false;
}
}
mSilentLog = FALSE;
// The offset is just past the au header
if( !file.Seek(sizeof(auHeader)) ||
file.Read(data.get(), mSummaryInfo.totalSummaryBytes) !=
mSummaryInfo.totalSummaryBytes ) {
memset(data.get(), 0, mSummaryInfo.totalSummaryBytes);
return false;
}
FixSummary(data.get());
return true;
}
}
/// Read the data portion of the block file using libsndfile. Convert it
/// to the given format if it is not already.
///
/// @param data The buffer where the data will be stored
/// @param format The format the data will be stored in
/// @param start The offset in this block file
/// @param len The number of samples to read
size_t SimpleBlockFile::ReadData(samplePtr data, sampleFormat format,
size_t start, size_t len, bool mayThrow) const
{
if (mCache.active)
{
//wxLogDebug("SimpleBlockFile::ReadData(): Data are already in cache.");
auto framesRead = std::min(len, std::max(start, mLen) - start);
CopySamples(
(samplePtr)(mCache.sampleData.get() +
start * SAMPLE_SIZE(mCache.format)),
mCache.format, data, format, framesRead);
if ( framesRead < len ) {
if (mayThrow)
// Not the best exception class?
throw FileException{ FileException::Cause::Read, mFileName };
ClearSamples(data, format, framesRead, len - framesRead);
}
return framesRead;
}
else
return CommonReadData( mayThrow,
mFileName, mSilentLog, nullptr, 0, 0, data, format, start, len);
}
void SimpleBlockFile::SaveXML(XMLWriter &xmlFile)
// may throw
{
xmlFile.StartTag(wxT("simpleblockfile"));
xmlFile.WriteAttr(wxT("filename"), mFileName.GetFullName());
xmlFile.WriteAttr(wxT("len"), mLen);
xmlFile.WriteAttr(wxT("min"), mMin);
xmlFile.WriteAttr(wxT("max"), mMax);
xmlFile.WriteAttr(wxT("rms"), mRMS);
xmlFile.EndTag(wxT("simpleblockfile"));
}
// BuildFromXML methods should always return a BlockFile, not NULL,
// even if the result is flawed (e.g., refers to nonexistent file),
// as testing will be done in DirManager::ProjectFSCK().
/// static
BlockFilePtr SimpleBlockFile::BuildFromXML(DirManager &dm, const wxChar **attrs)
{
wxFileNameWrapper fileName;
float min = 0.0f, max = 0.0f, rms = 0.0f;
size_t len = 0;
double dblValue;
long nValue;
while(*attrs)
{
const wxChar *attr = *attrs++;
const wxChar *value = *attrs++;
if (!value)
break;
const wxString strValue = value;
if (!wxStricmp(attr, wxT("filename")) &&
// Can't use XMLValueChecker::IsGoodFileName here, but do part of its test.
XMLValueChecker::IsGoodFileString(strValue) &&
(strValue.length() + 1 + dm.GetProjectDataDir().length() <= PLATFORM_MAX_PATH))
{
if (!dm.AssignFile(fileName, strValue, false))
// Make sure fileName is back to uninitialized state so we can detect problem later.
fileName.Clear();
}
else if (!wxStrcmp(attr, wxT("len")) &&
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue) &&
nValue > 0)
len = nValue;
else if (XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue))
{ // double parameters
if (!wxStricmp(attr, wxT("min")))
min = dblValue;
else if (!wxStricmp(attr, wxT("max")))
max = dblValue;
else if (!wxStricmp(attr, wxT("rms")) && (dblValue >= 0.0))
rms = dblValue;
}
}
return make_blockfile<SimpleBlockFile>
(std::move(fileName), len, min, max, rms);
}
/// Create a copy of this BlockFile, but using a different disk file.
///
/// @param newFileName The name of the NEW file to use.
BlockFilePtr SimpleBlockFile::Copy(wxFileNameWrapper &&newFileName)
{
auto newBlockFile = make_blockfile<SimpleBlockFile>
(std::move(newFileName), mLen, mMin, mMax, mRMS);
return newBlockFile;
}
auto SimpleBlockFile::GetSpaceUsage() const -> DiskByteCount
{
if (mCache.active && mCache.needWrite)
{
// We don't know space usage yet
return 0;
}
// Don't know the format, so it must be read from the file
if (mFormat == (sampleFormat) 0)
{
// Check sample format
wxFFile file(mFileName.GetFullPath(), wxT("rb"));
if (!file.IsOpened())
{
// Don't read into cache if file not available
return 0;
}
auHeader header;
if (file.Read(&header, sizeof(header)) != sizeof(header))
{
// Corrupt file
return 0;
}
wxUint32 encoding;
if (header.magic == 0x2e736e64)
encoding = header.encoding; // correct endianness
else
encoding = SwapUintEndianess(header.encoding);
switch (encoding)
{
case AU_SAMPLE_FORMAT_16:
mFormat = int16Sample;
break;
case AU_SAMPLE_FORMAT_24:
mFormat = int24Sample;
break;
default:
// floatSample is a safe default (we will never loose data)
mFormat = floatSample;
break;
}
file.Close();
}
return (
sizeof(auHeader) +
mSummaryInfo.totalSummaryBytes +
(GetLength() * SAMPLE_SIZE_DISK(mFormat))
);
}
void SimpleBlockFile::Recover(){
wxFFile file(mFileName.GetFullPath(), wxT("wb"));
if( !file.IsOpened() ){
// Can't do anything else.
return;
}
auHeader header;
header.magic = 0x2e736e64;
header.dataOffset = sizeof(auHeader) + mSummaryInfo.totalSummaryBytes;
// dataSize is optional, and we opt out
header.dataSize = 0xffffffff;
header.encoding = AU_SAMPLE_FORMAT_16;
header.sampleRate = 44100;
header.channels = 1;
file.Write(&header, sizeof(header));
for(decltype(mSummaryInfo.totalSummaryBytes) i = 0;
i < mSummaryInfo.totalSummaryBytes; i++)
file.Write(wxT("\0"),1);
for(decltype(mLen) i = 0; i < mLen * 2; i++)
file.Write(wxT("\0"),1);
}
void SimpleBlockFile::WriteCacheToDisk()
{
if (!GetNeedWriteCacheToDisk())
return;
if (WriteSimpleBlockFile(mCache.sampleData.get(), mLen, mCache.format,
mCache.summaryData.get()))
mCache.needWrite = false;
}
bool SimpleBlockFile::GetNeedWriteCacheToDisk()
{
return mCache.active && mCache.needWrite;
}
bool SimpleBlockFile::GetCache()
{
#ifdef DEPRECATED_AUDIO_CACHE
// See http://bugzilla.audacityteam.org/show_bug.cgi?id=545.
bool cacheBlockFiles = false;
gPrefs->Read(wxT("/Directories/CacheBlockFiles"), &cacheBlockFiles);
if (!cacheBlockFiles)
return false;
int lowMem = gPrefs->Read(wxT("/Directories/CacheLowMem"), 16l);
if (lowMem < 16) {
lowMem = 16;
}
lowMem <<= 20;
return (GetFreeMemory() > lowMem);
#else
return false;
#endif
}