diff --git a/Audio/WavFile.cpp b/Audio/WavFile.cpp new file mode 100644 index 000000000..89b2c5947 --- /dev/null +++ b/Audio/WavFile.cpp @@ -0,0 +1,379 @@ +#include "WavFile.hpp" + +#include +#include + +#include +#include +#include + +#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 (this);} + char const * operator & () const {return &*this;} + + std::array 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 (outer_desc.size_) : qFromLittleEndian (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 (wave_desc.size_) : qFromLittleEndian (wave_desc.size_); + if (!memcmp (&wave_desc.id_, "fmt ", 4)) + { + FormatChunk fmt; + if (file_.read (reinterpret_cast (&fmt), sizeof fmt) != sizeof fmt) return false; + auto audio_format = be ? qFromBigEndian (fmt.audio_format) : qFromLittleEndian (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 (fmt.num_channels) : qFromLittleEndian (fmt.num_channels)); + format_.setCodec ("audio/pcm"); + format_.setSampleRate (be ? qFromBigEndian (fmt.sample_rate) : qFromLittleEndian (fmt.sample_rate)); + int bits_per_sample {be ? qFromBigEndian (fmt.bits_per_sample) : qFromLittleEndian (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 (info_desc.size_) : qFromLittleEndian (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 (1); // PCM + fmt.num_channels = qToBigEndian (format.channelCount ()); + fmt.sample_rate = qToBigEndian (format.sampleRate ()); + fmt.byte_rate = qToBigEndian (format.bytesForDuration (1000)); + fmt.block_align = qToBigEndian (format.bytesPerFrame ()); + fmt.bits_per_sample = qToBigEndian (format.sampleSize ()); + desc.set ("fmt", qToBigEndian (sizeof fmt)); + } + else + { + fmt.audio_format = qToLittleEndian (1); // PCM + fmt.num_channels = qToLittleEndian (format.channelCount ()); + fmt.sample_rate = qToLittleEndian (format.sampleRate ()); + fmt.byte_rate = qToLittleEndian (format.bytesForDuration (1000)); + fmt.block_align = qToLittleEndian (format.bytesPerFrame ()); + fmt.bits_per_sample = qToLittleEndian (format.sampleSize ()); + desc.set ("fmt", qToLittleEndian (sizeof fmt)); + } + if (file_.write (&desc, sizeof desc) != sizeof desc) return false; + if (file_.write (reinterpret_cast (&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 (len) : qToLittleEndian (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 (size) : qToLittleEndian (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 (size) : qToLittleEndian (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 (size) : qToLittleEndian (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 (size) : qToLittleEndian (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; +} diff --git a/Audio/WavFile.hpp b/Audio/WavFile.hpp new file mode 100644 index 000000000..bfba00f46 --- /dev/null +++ b/Audio/WavFile.hpp @@ -0,0 +1,83 @@ +#ifndef WSV_FILE_HPP__ +#define WSV_FILE_HPP__ + +#include + +#include +#include +#include +#include + +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, 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index af1474e32..a6a3cb187 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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})