Class to read and write WAV format files

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6337 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
Bill Somerville 2016-01-02 17:19:49 +00:00
parent 92e1f70b8f
commit 58aacc590e
3 changed files with 477 additions and 5 deletions

379
Audio/WavFile.cpp Normal file
View File

@ -0,0 +1,379 @@
#include "WavFile.hpp"
#include <cstring>
#include <numeric>
#include <qendian.h>
#include <QAudioFormat>
#include <QDebug>
#include "moc_WavFile.cpp"
namespace
{
struct Desc
{
Desc () = default;
explicit Desc (char const * id, quint32 size = 0)
: size_ {size}
{
set (id);
}
void set (char const * id = nullptr)
{
if (id)
{
auto len = std::min (4u, strlen (id));
memcpy (id_.data (), id, len);
memset (id_.data () + len, ' ', 4u - len);
}
else
{
memcpy (id_.data (), "JUNK", 4);
}
}
void set (char const * id, quint32 size)
{
set (id);
size_ = size;
}
char * operator & () {return reinterpret_cast<char *> (this);}
char const * operator & () const {return &*this;}
std::array<char, 4> id_;
quint32 size_;
};
struct FormatChunk
{
quint16 audio_format;
quint16 num_channels;
quint32 sample_rate;
quint32 byte_rate;
quint16 block_align;
quint16 bits_per_sample;
};
}
WavFile::WavFile (QAudioFormat const& format, QObject * parent)
: QIODevice {parent}
, header_dirty_ {true}
, format_ {format}
, header_length_ {-1}
{
}
WavFile::WavFile (QAudioFormat const& format, QString const& name, QObject * parent)
: QIODevice {parent}
, header_dirty_ {true}
, format_ {format}
, file_ {name}
, header_length_ {-1}
{
}
WavFile::WavFile (QAudioFormat const& format, QString const& name
, InfoDictionary const& dictionay, QObject * parent)
: QIODevice {parent}
, header_dirty_ {true}
, format_ {format}
, file_ {name}
, header_length_ {-1}
, info_dictionary_ {dictionay}
{
}
WavFile::~WavFile ()
{
QIODevice::close ();
if (header_dirty_) update_header ();
file_.close ();
}
bool WavFile::open (OpenMode mode)
{
bool result {false};
if (!(mode & ReadOnly)) return result;
if (!(mode & WriteOnly))
{
result = file_.open (mode & ~Text) && read_header ();
}
else
{
if ((result = file_.open (mode & ~Text)))
{
if (!(result = read_header () || write_header (format_)))
{
file_.close ();
return false;
}
}
}
return result ? initialize (mode) : false;
}
bool WavFile::open(FILE * fh, OpenMode mode, FileHandleFlags flags)
{
bool result {false};
if (!(mode & ReadOnly)) return result;
if (!mode & WriteOnly)
{
result = file_.open (fh, mode & ~Text, flags) && read_header ();
}
else
{
if ((result = file_.open (fh, mode & ~Text, flags)))
{
if (!(result = read_header () || write_header (format_)))
{
file_.close ();
return false;
}
}
}
return result ? initialize (mode) : false;
}
bool WavFile::open (int fd, OpenMode mode, FileHandleFlags flags)
{
bool result {false};
if (!(mode & ReadOnly)) return result;
if (!(mode & WriteOnly))
{
result = file_.open (fd, mode & ~Text, flags) && read_header ();
}
else
{
if ((result = file_.open (fd, mode & ~Text, flags)))
{
if (!(result = read_header () || write_header (format_)))
{
file_.close ();
return false;
}
}
}
return result ? initialize (mode) : false;
}
bool WavFile::initialize (OpenMode mode)
{
bool result {QIODevice::open (mode | Unbuffered)};
if (result && (mode & Append))
{
result = file_.seek (file_.size ());
if (result) result = seek (file_.size () - header_length_);
}
else
{
result = seek (0);
}
if (!result)
{
file_.close ();
close ();
}
return result;
}
bool WavFile::read_header ()
{
if (!file_.seek (0)) return false;
Desc outer_desc;
auto outer_offset = file_.pos ();
quint32 outer_size {0};
bool be {false};
while (outer_offset < sizeof outer_desc + outer_desc.size_ - 1) // allow for uncounted pad
{
if (file_.read (&outer_desc, sizeof outer_desc) != sizeof outer_desc) return false;
be = !memcmp (&outer_desc.id_, "RIFX", 4);
outer_size = be ? qFromBigEndian<quint32> (outer_desc.size_) : qFromLittleEndian<quint32> (outer_desc.size_);
if (!memcmp (&outer_desc.id_, "RIFF", 4) || be)
{
// RIFF or RIFX
char riff_item[4];
if (file_.read (riff_item, sizeof riff_item) != sizeof riff_item) return false;
if (!memcmp (riff_item, "WAVE", 4))
{
// WAVE
Desc wave_desc;
auto wave_offset = file_.pos ();
quint32 wave_size {0};
while (wave_offset < outer_offset + sizeof outer_desc + outer_size - 1)
{
if (file_.read (&wave_desc, sizeof wave_desc) != sizeof wave_desc) return false;
wave_size = be ? qFromBigEndian<quint32> (wave_desc.size_) : qFromLittleEndian<quint32> (wave_desc.size_);
if (!memcmp (&wave_desc.id_, "fmt ", 4))
{
FormatChunk fmt;
if (file_.read (reinterpret_cast<char *> (&fmt), sizeof fmt) != sizeof fmt) return false;
auto audio_format = be ? qFromBigEndian<quint16> (fmt.audio_format) : qFromLittleEndian<quint16> (fmt.audio_format);
if (audio_format != 0 && audio_format != 1) return false; // not PCM nor undefined
format_.setByteOrder (be ? QAudioFormat::BigEndian : QAudioFormat::LittleEndian);
format_.setChannelCount (be ? qFromBigEndian<quint16> (fmt.num_channels) : qFromLittleEndian<quint16> (fmt.num_channels));
format_.setCodec ("audio/pcm");
format_.setSampleRate (be ? qFromBigEndian<quint32> (fmt.sample_rate) : qFromLittleEndian<quint32> (fmt.sample_rate));
int bits_per_sample {be ? qFromBigEndian<quint16> (fmt.bits_per_sample) : qFromLittleEndian<quint16> (fmt.bits_per_sample)};
format_.setSampleSize (bits_per_sample);
format_.setSampleType (8 == bits_per_sample ? QAudioFormat::UnSignedInt : QAudioFormat::SignedInt);
}
else if (!memcmp (&wave_desc.id_, "data", 4))
{
header_length_ = file_.pos ();
return true; // done
}
else if (!memcmp (&wave_desc.id_, "LIST", 4))
{
char list_type[4];
if (file_.read (list_type, sizeof list_type) != sizeof list_type) return false;
if (!memcmp (list_type, "INFO", 4))
{
Desc info_desc;
auto info_offset = file_.pos ();
quint32 info_size {0};
while (info_offset < wave_offset + sizeof wave_desc + wave_size - 1)
{
if (file_.read (&info_desc, sizeof info_desc) != sizeof info_desc) return false;
info_size = be ? qFromBigEndian<quint32> (info_desc.size_) : qFromLittleEndian<quint32> (info_desc.size_);
info_dictionary_[info_desc.id_] = file_.read (info_size);
if (!file_.seek (info_offset + sizeof info_desc + (info_size + 1) / 2 * 2)) return false;;
info_offset = file_.pos ();
}
}
}
if (!file_.seek (wave_offset + sizeof wave_desc + (wave_size + 1) / 2 * 2)) return false;
wave_offset = file_.pos ();
}
}
}
if (!file_.seek (outer_offset + sizeof outer_desc + (outer_size + 1) / 2 * 2)) return false;
outer_offset = file_.pos ();
}
return false;
}
bool WavFile::write_header (QAudioFormat format)
{
if ("audio/pcm" != format.codec ()) return false;
if (!file_.seek (0)) return false;
header_length_ = 0;
bool be {QAudioFormat::BigEndian == format_.byteOrder ()};
Desc desc {be ? "RIFX" : "RIFF"};
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
header_dirty_ = true;
if (file_.write ("WAVE", 4) != 4) return false;
FormatChunk fmt;
if (be)
{
fmt.audio_format = qToBigEndian<quint16> (1); // PCM
fmt.num_channels = qToBigEndian<quint16> (format.channelCount ());
fmt.sample_rate = qToBigEndian<quint32> (format.sampleRate ());
fmt.byte_rate = qToBigEndian<quint32> (format.bytesForDuration (1000));
fmt.block_align = qToBigEndian<quint16> (format.bytesPerFrame ());
fmt.bits_per_sample = qToBigEndian<quint16> (format.sampleSize ());
desc.set ("fmt", qToBigEndian<quint32> (sizeof fmt));
}
else
{
fmt.audio_format = qToLittleEndian<quint16> (1); // PCM
fmt.num_channels = qToLittleEndian<quint16> (format.channelCount ());
fmt.sample_rate = qToLittleEndian<quint32> (format.sampleRate ());
fmt.byte_rate = qToLittleEndian<quint32> (format.bytesForDuration (1000));
fmt.block_align = qToLittleEndian<quint16> (format.bytesPerFrame ());
fmt.bits_per_sample = qToLittleEndian<quint16> (format.sampleSize ());
desc.set ("fmt", qToLittleEndian<quint32> (sizeof fmt));
}
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
if (file_.write (reinterpret_cast<char const *> (&fmt), sizeof fmt) != sizeof fmt) return false;
if (info_dictionary_.size ())
{
auto position = file_.pos ();
desc.set ("LIST");
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
if (file_.write ("INFO", 4) != 4) return false;
for (auto iter = info_dictionary_.constBegin ()
; iter != info_dictionary_.constEnd (); ++iter)
{
auto value = iter.value ();
auto len = value.size () + 1;
auto padded_len = (len + 1) / 2 * 2;
if (padded_len > value.size ()) value.append ('\0');
desc.set (iter.key ().data (), be ? qToBigEndian<quint32> (len) : qToLittleEndian<quint32> (len));
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
if (file_.write (value.constData (), padded_len) != padded_len) return false;
}
auto end_position = file_.pos ();
if (!file_.seek (position)) return false;
if (file_.peek (&desc, sizeof desc) != sizeof desc) return false;
Q_ASSERT (!memcmp (desc.id_.data (), "LIST", 4));
auto size = end_position - position - sizeof desc;
desc.size_ = be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (size);
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
if (!file_.seek (end_position)) return false;
}
auto size = file_.size () - file_.pos () - sizeof desc;
desc.set ("data", be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (size));
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
header_length_ = file_.pos ();
return true;
}
bool WavFile::update_header ()
{
if (header_length_ < 0 || !(file_.openMode () & WriteOnly)) return false;
auto position = file_.pos ();
bool be {QAudioFormat::BigEndian == format_.byteOrder ()};
Desc desc;
if (!file_.seek (header_length_ - sizeof desc)) return false;
if (file_.peek (&desc, sizeof desc) != sizeof desc) return false;
Q_ASSERT (!memcmp (desc.id_.data (), "data", 4));
auto size = file_.size () - header_length_;
desc.size_ = be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (size);
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
if (!file_.seek (0)) return false;
if (file_.peek (&desc, sizeof desc) != sizeof desc) return false;
Q_ASSERT (!memcmp (desc.id_.data (), "RIFF", 4) || !memcmp (desc.id_.data (), "RIFX", 4));
size = file_.size () - sizeof desc;
desc.size_ = be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (size);
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
return file_.seek (position);
}
bool WavFile::reset ()
{
file_.seek (header_length_);
return QIODevice::reset ();
}
bool WavFile::isSequential () const
{
return file_.isSequential ();
}
void WavFile::close ()
{
QIODevice::close ();
file_.close ();
}
bool WavFile::seek (qint64 pos)
{
if (pos < 0) return false;
QIODevice::seek (pos);
return file_.seek (pos + header_length_);
}
qint64 WavFile::readData (char * data, qint64 max_size)
{
return file_.read (data, max_size);
}
qint64 WavFile::writeData (char const* data, qint64 max_size)
{
auto bytes = file_.write (data, max_size);
if (bytes > 0 && atEnd ()) header_dirty_ = true;
return bytes;
}

83
Audio/WavFile.hpp Normal file
View File

@ -0,0 +1,83 @@
#ifndef WSV_FILE_HPP__
#define WSV_FILE_HPP__
#include <array>
#include <QFile>
#include <QAudioFormat>
#include <QMap>
#include <QByteArray>
class QObject;
class QString;
class WavFile final
: public QIODevice
{
Q_OBJECT
public:
using FileHandleFlags = QFile::FileHandleFlags;
using Permissions = QFile::Permissions;
using FileError = QFile::FileError;
using MemoryMapFlags = QFile::MemoryMapFlags;
using InfoDictionary = QMap<std::array<char, 4>, QByteArray>;
explicit WavFile (QAudioFormat const&, QObject * parent = nullptr);
explicit WavFile (QAudioFormat const&, QString const& name, QObject * parent = nullptr);
explicit WavFile (QAudioFormat const&, QString const& name, InfoDictionary const&, QObject * parent = nullptr);
~WavFile ();
QAudioFormat const& format () const {return format_;}
qint64 header_length () const {return header_length_;}
InfoDictionary const& info () const {return info_dictionary_;}
// Emulate QFile interface
bool open (OpenMode) override;
bool open (FILE *, OpenMode, FileHandleFlags = QFile::DontCloseHandle);
bool open (int fd, OpenMode, FileHandleFlags = QFile::DontCloseHandle);
bool copy (QString const& new_name);
// forward to QFile
bool exists () const {return file_.exists ();}
bool link (QString const& link_name) {return file_.link (link_name);}
bool remove () {return file_.remove ();}
bool rename (QString const& new_name) {return file_.rename (new_name);}
void setFileName (QString const& name) {file_.setFileName (name);}
QString symLinkTarget () const {return file_.symLinkTarget ();}
QString fileName () const {return file_.fileName ();}
Permissions permissions () const {return file_.permissions ();}
bool resize (qint64 new_size) {return file_.resize (new_size + header_length_);}
bool setPermissions (Permissions permissions) {return file_.setPermissions (permissions);}
FileError error () const {return file_.error ();}
bool flush () {return file_.flush ();}
int handle () const {return file_.handle ();}
uchar * map (qint64 offset, qint64 size, MemoryMapFlags flags = QFile::NoOptions)
{
return file_.map (offset, size, flags);
}
bool unmap (uchar * address) {return file_.unmap (address);}
void unsetError () {file_.unsetError ();}
// QIODevice overrides
bool isSequential () const override;
bool reset () override;
bool seek (qint64) override;
void close () override;
protected:
qint64 readData (char * data, qint64 max_size) override;
qint64 writeData (char const* data, qint64 max_size) override;
private:
bool initialize (OpenMode);
bool read_header ();
bool write_header (QAudioFormat);
bool update_header ();
bool header_dirty_;
QAudioFormat format_;
QFile file_;
qint64 header_length_;
InfoDictionary info_dictionary_;
};
#endif

View File

@ -217,6 +217,10 @@ set (wsjt_qt_CXXSRCS
SampleDownloader/RemoteFile.cpp
)
set (wsjt_qtmm_CXXSRCS
Audio/WavFile.cpp
)
set (jt9_CXXSRCS
lib/ipcomm.cpp
)
@ -486,6 +490,7 @@ set (UDPDaemon_CXXSRCS
set (all_CXXSRCS
${wsjt_CXXSRCS}
${wsjt_qt_CXXSRCS}
${wsjt_qtmm_CXXSRCS}
${jt9_CXXSRCS}
${wsjtx_CXXSRCS}
${message_aggregator_CXXSRCS}
@ -890,8 +895,10 @@ add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS})
# build an OpenMP variant of the Fortran library routines
add_library (wsjt_fort STATIC ${wsjt_FSRCS})
target_link_libraries (wsjt_fort ${FFTW3_LIBRARIES})
if (${OPENMP_FOUND} OR APPLE)
add_library (wsjt_fort_omp STATIC ${wsjt_FSRCS})
target_link_libraries (wsjt_fort_omp ${FFTW3_LIBRARIES})
set_target_properties (wsjt_fort_omp
PROPERTIES
COMPILE_FLAGS "${OpenMP_C_FLAGS}"
@ -917,11 +924,14 @@ if (WIN32)
target_link_libraries (wsjt_qt Qt5::AxContainer Qt5::AxBase)
endif (WIN32)
add_library (wsjt_qtmm STATIC ${wsjt_qtmm_CXXSRCS} ${wsjt_qtmm_GENUISRCS})
target_link_libraries (wsjt_qtmm Qt5::Multimedia)
add_executable (jt4sim lib/jt4sim.f90 wsjtx.rc)
target_link_libraries (jt4sim wsjt_fort wsjt_cxx)
add_executable (jt65sim lib/jt65sim.f90 wsjtx.rc)
target_link_libraries (jt65sim wsjt_fort wsjt_cxx ${FFTW3_LIBRARIES})
target_link_libraries (jt65sim wsjt_fort wsjt_cxx)
add_executable (jt9sim lib/jt9sim.f90 wsjtx.rc)
target_link_libraries (jt9sim wsjt_fort wsjt_cxx)
@ -942,7 +952,7 @@ add_executable (jt4code lib/jt4code.f90 wsjtx.rc)
target_link_libraries (jt4code wsjt_fort wsjt_cxx)
add_executable (jt65 lib/jt65.f90 lib/jt65_test.f90 wsjtx.rc)
target_link_libraries (jt65 wsjt_fort wsjt_cxx ${FFTW3_LIBRARIES})
target_link_libraries (jt65 wsjt_fort wsjt_cxx)
add_executable (jt9 lib/jt9.f90 lib/jt9a.f90 ${jt9_CXXSRCS} wsjtx.rc)
if (${OPENMP_FOUND} OR APPLE)
@ -964,9 +974,9 @@ if (${OPENMP_FOUND} OR APPLE)
Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
)
endif (APPLE)
target_link_libraries (jt9 wsjt_fort_omp wsjt_cxx ${FFTW3_LIBRARIES} Qt5::Core)
target_link_libraries (jt9 wsjt_fort_omp wsjt_cxx Qt5::Core)
else (${OPENMP_FOUND} OR APPLE)
target_link_libraries (jt9 wsjt_fort wsjt_cxx ${FFTW3_LIBRARIES} Qt5::Core)
target_link_libraries (jt9 wsjt_fort wsjt_cxx Qt5::Core)
endif (${OPENMP_FOUND} OR APPLE)
# build the main application
@ -993,7 +1003,7 @@ set_target_properties (wsjtx PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
)
target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES} Qt5::Multimedia)
target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
qt5_use_modules (wsjtx SerialPort) # not sure why the interface link library syntax above doesn't work
add_resources (message_aggregator_RESOURCES /qss ${message_aggregator_STYLESHEETS})