From 4e6de783b0e6dc93cc1f812f8c45e4bd03b51ef8 Mon Sep 17 00:00:00 2001 From: Bill Somerville Date: Mon, 11 Jan 2016 15:00:43 +0000 Subject: [PATCH] Use new WAV file class (BWFFile) read and write WAV files Saved WAV files now contain some useful metadata. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6383 ab8295b8-cf94-4d9e-aec4-7959e3be5d79 --- Audio/WavFile.cpp | 778 +++++++++++++++++++++++++++++++++++--------- Audio/WavFile.hpp | 210 +++++++++--- lib/jt65_decode.f90 | 458 +++++++++++++------------- mainwindow.cpp | 100 ++++-- mainwindow.h | 14 +- 5 files changed, 1098 insertions(+), 462 deletions(-) diff --git a/Audio/WavFile.cpp b/Audio/WavFile.cpp index 43e1085cf..116f38f36 100644 --- a/Audio/WavFile.cpp +++ b/Audio/WavFile.cpp @@ -6,12 +6,19 @@ #include #include -#include +#include +#include +#include +#include +#include + +#include "pimpl_impl.hpp" #include "moc_WavFile.cpp" namespace { + // chunk descriptor struct Desc { Desc () = default; @@ -48,6 +55,7 @@ namespace quint32 size_; }; + // "fmt " chunk contents struct FormatChunk { quint16 audio_format; @@ -57,131 +65,133 @@ namespace quint16 block_align; quint16 bits_per_sample; }; + + // "bext" chunk contents + struct BroadcastAudioExtension + { + using Version = BWFFile::BextVersion; + using UMID = BWFFile::UMID; + + BroadcastAudioExtension (Version version = Version::v_0) + : version_ {static_cast (version)} + , umid_ {{}} + { + // set some sensible defaults for the "bext" fields + auto now = QDateTime::currentDateTimeUtc (); + std::strncpy (origination_date_, + now.date ().toString ("yyyy-MM-dd").toLocal8Bit ().constData (), + sizeof origination_date_); + std::strncpy (origination_time_, + now.time ().toString ("hh-mm-ss").toLocal8Bit ().constData (), + sizeof origination_time_); + auto uuid = QUuid::createUuid ().toRfc4122 (); + std::copy (uuid.cbegin (), uuid.cend (), umid_.data () + 16); + } + + char description_[256]; + char originator_[32]; + char originator_reference_[32]; + char origination_date_[10]; + char origination_time_[10]; + quint32 time_reference_low_; + quint32 time_reference_high_; + quint16 version_; + UMID umid_; // V1 zero for V0 + quint16 loudness_value_; // V2 + quint16 loudness_range_; // V2 + quint16 max_true_peak_level_; // V2 + quint16 max_momentary_loudness_; // V2 + quint16 max_short_term_loudness_; // V2 + quint8 reserved_[180]; + char coding_history_[]; + }; } -WavFile::WavFile (QAudioFormat const& format, QObject * parent) - : QIODevice {parent} - , header_dirty_ {true} - , format_ {format} - , header_length_ {-1} +class BWFFile::impl final { -} +public: + impl (QAudioFormat const& format) + : header_dirty_ {true} + , format_ {format} + , header_length_ {-1} + , data_size_ {-1} + { + } -WavFile::WavFile (QAudioFormat const& format, QString const& name, QObject * parent) - : QIODevice {parent} - , header_dirty_ {true} - , format_ {format} - , file_ {name} - , header_length_ {-1} -{ -} + impl (QAudioFormat const& format, QString const& name) + : header_dirty_ {true} + , format_ {format} + , file_ {name} + , header_length_ {-1} + , data_size_ {-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} -{ -} + impl (QAudioFormat const& format, QString const& name, InfoDictionary const& dictionary) + : header_dirty_ {true} + , format_ {format} + , file_ {name} + , info_dictionary_ {dictionary} + , header_length_ {-1} + , data_size_ {-1} + { + } -WavFile::~WavFile () -{ - QIODevice::close (); - if (header_dirty_) update_header (); - file_.close (); -} + ~impl () + { + file_.close (); + } -bool WavFile::open (OpenMode mode) + bool initialize (BWFFile * self, OpenMode mode); + bool read_header (); + bool write_header (QAudioFormat); + bool update_header (); + + BroadcastAudioExtension const * bext () const + { + return bext_.isEmpty () ? nullptr : reinterpret_cast (bext_.constData ()); + } + + BroadcastAudioExtension * bext () + { + if (bext_.isEmpty ()) // create a "bext" chunk in place + { + bext_.data (); + bext_.fill ('\0', sizeof (BroadcastAudioExtension)); + new (bext_.data ()) BroadcastAudioExtension {}; + } + return reinterpret_cast (bext_.data ()); + } + + bool header_dirty_; + QAudioFormat format_; + QFile file_; + QByteArray bext_; + InfoDictionary info_dictionary_; + qint64 header_length_; + qint64 data_size_; +}; + +bool BWFFile::impl::initialize (BWFFile * self, 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)) + if (mode & Append) { result = file_.seek (file_.size ()); - if (result) result = seek (file_.size () - header_length_); + if (result) result = self->seek (file_.size () - header_length_); } else { - result = seek (0); - } - if (!result) - { - file_.close (); - close (); + result = self->seek (0); } return result; } -bool WavFile::read_header () +bool BWFFile::impl::read_header () { + header_length_ = -1; + data_size_ = -1; + if (!(file_.openMode () & ReadOnly)) return false; if (!file_.seek (0)) return false; Desc outer_desc; quint32 outer_offset = file_.pos (); @@ -207,6 +217,10 @@ bool WavFile::read_header () { 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_, "bext", 4)) + { + bext_ = file_.read (wave_size); + } if (!memcmp (&wave_desc.id_, "fmt ", 4)) { FormatChunk fmt; @@ -223,8 +237,8 @@ bool WavFile::read_header () } else if (!memcmp (&wave_desc.id_, "data", 4)) { + data_size_ = wave_size; header_length_ = file_.pos (); - return true; // done } else if (!memcmp (&wave_desc.id_, "LIST", 4)) { @@ -253,12 +267,14 @@ bool WavFile::read_header () if (!file_.seek (outer_offset + sizeof outer_desc + (outer_size + 1) / 2 * 2)) return false; outer_offset = file_.pos (); } - return false; + return data_size_ >= 0 && file_.seek (header_length_); } -bool WavFile::write_header (QAudioFormat format) +bool BWFFile::impl::write_header (QAudioFormat format) { + data_size_ = -1; if ("audio/pcm" != format.codec ()) return false; + if (!(file_.openMode () & WriteOnly)) return false; if (!file_.seek (0)) return false; header_length_ = 0; bool be {QAudioFormat::BigEndian == format_.byteOrder ()}; @@ -266,6 +282,7 @@ bool WavFile::write_header (QAudioFormat format) 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) { @@ -289,92 +306,533 @@ bool WavFile::write_header (QAudioFormat format) } 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)); + + desc.set ("data"); if (file_.write (&desc, sizeof desc) != sizeof desc) return false; header_length_ = file_.pos (); return true; } -bool WavFile::update_header () +bool BWFFile::impl::update_header () { if (header_length_ < 0 || !(file_.openMode () & WriteOnly)) return false; auto position = file_.pos (); bool be {QAudioFormat::BigEndian == format_.byteOrder ()}; Desc desc; + auto size = data_size_ < 0 ? file_.size () - header_length_ : data_size_; 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); + desc.set ("data", 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)); + + if (!bext_.isEmpty ()) + { + if (!file_.seek (file_.size ())) return false; + auto size = bext_.size (); + desc.set ("bext", be ? qToBigEndian (size) : qToLittleEndian (size)); + if ((file_.size () % 2) && file_.write ("\0", 1) != 1) return false; + if (file_.write (&desc, sizeof desc) != sizeof desc) return false; + auto * data = reinterpret_cast (bext_.data ()); + if (be) + { + data->time_reference_low_ = qToBigEndian (data->time_reference_low_); + data->time_reference_high_ = qToBigEndian (data->time_reference_high_); + switch (static_cast (data->version_)) + { + case BextVersion::v_0: + data->version_ = qToBigEndian (data->version_); + default: + data->loudness_value_ = qToBigEndian (data->loudness_value_); + data->loudness_range_ = qToBigEndian (data->loudness_range_); + data->max_true_peak_level_ = qToBigEndian (data->max_true_peak_level_); + data->max_momentary_loudness_ = qToBigEndian (data->max_momentary_loudness_); + data->max_short_term_loudness_ = qToBigEndian (data->max_short_term_loudness_); + } + } + else + { + data->time_reference_low_ = qToLittleEndian (data->time_reference_low_); + data->time_reference_high_ = qToLittleEndian (data->time_reference_high_); + switch (static_cast (data->version_)) + { + case BextVersion::v_0: + data->version_ = qToLittleEndian (data->version_); + default: + data->loudness_value_ = qToLittleEndian (data->loudness_value_); + data->loudness_range_ = qToLittleEndian (data->loudness_range_); + data->max_true_peak_level_ = qToLittleEndian (data->max_true_peak_level_); + data->max_momentary_loudness_ = qToLittleEndian (data->max_momentary_loudness_); + data->max_short_term_loudness_ = qToLittleEndian (data->max_short_term_loudness_); + } + } + if (file_.write (bext_) != size) return false; + } + + if (info_dictionary_.size ()) + { + if (!file_.seek (file_.size ())) return false; + desc.set ("LIST"); + if ((file_.size () % 2) && file_.write ("\0", 1) != 1) return false; + if (file_.write (&desc, sizeof desc) != sizeof desc) return false; + auto list_start = file_.pos (); + 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; // include terminating null char + desc.set (iter.key ().data (), be ? qToBigEndian (len) : qToLittleEndian (len)); + if ((file_.size () % 2) && file_.write ("\0", 1) != 1) return false; + if (file_.write (&desc, sizeof desc) != sizeof desc) return false; + if (file_.write (value.constData (), len) != len) return false; + } + auto size = file_.pos () - list_start; + if (!file_.seek (list_start - sizeof desc)) return false; + desc.set ("LIST", be ? qToBigEndian (size) : qToLittleEndian (size)); + if (file_.write (&desc, sizeof desc) != sizeof desc) return false; + } + size = file_.size () - sizeof desc; - desc.size_ = be ? qToBigEndian (size) : qToLittleEndian (size); + if ((file_.size () % 2) && file_.seek (file_.size ()) && file_.write ("\0", 1) != 1) return false; + if (!file_.seek (0)) return false; + desc.set (be ? "RIFX" : "RIFF", be ? qToBigEndian (size) : qToLittleEndian (size)); if (file_.write (&desc, sizeof desc) != sizeof desc) return false; return file_.seek (position); } -bool WavFile::reset () +// +// BWFFile implementation +// +BWFFile::BWFFile (QAudioFormat const& format, QObject * parent) + : QIODevice {parent} + , m_ {format} { - file_.seek (header_length_); +} + +BWFFile::BWFFile (QAudioFormat const& format, QString const& name, QObject * parent) + : QIODevice {parent} + , m_ {format, name} +{ +} + +BWFFile::BWFFile (QAudioFormat const& format, QString const& name + , InfoDictionary const& dictionary, QObject * parent) + : QIODevice {parent} + , m_ {format, name, dictionary} +{ +} + +BWFFile::~BWFFile () +{ + if (isOpen ()) close (); +} + +bool BWFFile::open (OpenMode mode) +{ + bool result {false}; + if (!(mode & WriteOnly)) + { + result = m_->file_.open (mode & ~Text) && m_->read_header (); + } + else + { + if ((result = m_->file_.open (mode & ~Text))) + { + if (!(result = m_->read_header () + || m_->write_header (m_->format_) + || m_->file_.resize (m_->header_length_))) + { + m_->file_.close (); + return false; + } + } + } + if (result && (result = QIODevice::open (mode | Unbuffered))) + { + return m_->initialize (this, mode); + } + if (!result) close (); + return result; +} + +bool BWFFile::open(FILE * fh, OpenMode mode, FileHandleFlags flags) +{ + bool result {false}; + if (!(mode & ReadOnly)) return result; + if (!mode & WriteOnly) + { + result = m_->file_.open (fh, mode & ~Text, flags) && m_->read_header (); + } + else + { + if ((result = m_->file_.open (fh, mode & ~Text, flags))) + { + if (!(result = m_->read_header () + || m_->write_header (m_->format_) + || m_->file_.resize (m_->header_length_))) + { + m_->file_.close (); + return false; + } + } + } + if (result && (result = QIODevice::open (mode | Unbuffered))) + { + return m_->initialize (this, mode); + } + if (!result) close (); + return result; +} + +bool BWFFile::open (int fd, OpenMode mode, FileHandleFlags flags) +{ + bool result {false}; + if (!(mode & ReadOnly)) return result; + if (!(mode & WriteOnly)) + { + result = m_->file_.open (fd, mode & ~Text, flags) && m_->read_header (); + } + else + { + if ((result = m_->file_.open (fd, mode & ~Text, flags))) + { + if (!(result = m_->read_header () + || m_->write_header (m_->format_) + || m_->file_.resize (m_->header_length_))) + { + m_->file_.close (); + return false; + } + } + } + if (result && (result = QIODevice::open (mode | Unbuffered))) + { + return m_->initialize (this, mode); + } + if (!result) close (); + return result; +} + +QAudioFormat const& BWFFile::format () const {return m_->format_;} + +auto BWFFile::list_info () -> InfoDictionary& +{ + m_->header_dirty_ = true; + return m_->info_dictionary_; +} + + +// Broadcast Audio Extension fields +auto BWFFile::bext_version () const -> BextVersion +{ + return static_cast (m_->bext () ? 0 : m_->bext ()->version_); +} + +void BWFFile::bext_version (BextVersion version) +{ + m_->header_dirty_ = true; + m_->bext ()->version_ = static_cast (version); +} + +QByteArray BWFFile::bext_description () const +{ + if (!m_->bext ()) return {}; + return QByteArray::fromRawData (m_->bext ()->description_, strlen (m_->bext ()->description_)); +} + +void BWFFile::bext_description (QByteArray const& description) +{ + m_->header_dirty_ = true; + std::strncpy (m_->bext ()->description_, description.constData (), sizeof (BroadcastAudioExtension::description_)); +} + +QByteArray BWFFile::bext_originator () const +{ + if (!m_->bext ()) return {}; + return QByteArray::fromRawData (m_->bext ()->originator_, strlen (m_->bext ()->originator_)); +} + +void BWFFile::bext_originator (QByteArray const& originator) +{ + m_->header_dirty_ = true; + std::strncpy (m_->bext ()->originator_, originator.constData (), sizeof (BroadcastAudioExtension::originator_)); +} + +QByteArray BWFFile::bext_originator_reference () const +{ + if (!m_->bext ()) return {}; + return QByteArray::fromRawData (m_->bext ()->originator_reference_, strlen (m_->bext ()->originator_reference_)); +} + +void BWFFile::bext_originator_reference (QByteArray const& reference) +{ + m_->header_dirty_ = true; + std::strncpy (m_->bext ()->originator_reference_, reference.constData (), sizeof (BroadcastAudioExtension::originator_reference_)); +} + +QDateTime BWFFile::bext_origination_date_time () const +{ + if (!m_->bext ()) return {}; + return {QDate::fromString (m_->bext ()->origination_date_, "yyyy-MM-dd"), + QTime::fromString (m_->bext ()->origination_time_, "hh-mm-ss"), Qt::UTC}; +} + +void BWFFile::bext_origination_date_time (QDateTime const& dt) +{ + m_->header_dirty_ = true; + std::strncpy (m_->bext ()->origination_date_, + dt.date ().toString ("yyyy-MM-dd").toLocal8Bit ().constData (), + sizeof (BroadcastAudioExtension::origination_date_)); + std::strncpy (m_->bext ()->origination_time_, + dt.time ().toString ("hh-mm-ss").toLocal8Bit ().constData (), + sizeof (BroadcastAudioExtension::origination_time_)); +} + +quint64 BWFFile::bext_time_reference () const +{ + if (!m_->bext ()) return 0; + return (quint64 (m_->bext ()->time_reference_high_) << 32) + m_->bext ()->time_reference_low_; +} + +void BWFFile::bext_time_reference (quint64 time_code) +{ + m_->header_dirty_ = true; + m_->bext ()->time_reference_low_ = time_code & 0x00000000ffffffffll; + m_->bext ()->time_reference_high_ = time_code >> 32; +} + +auto BWFFile::bext_umid () const -> UMID +{ + UMID umid {'\0'}; + if (m_->bext ()) + { + umid = m_->bext ()->umid_; + } + return umid; +} + +void BWFFile::bext_umid (UMID const& umid) +{ + m_->header_dirty_ = true; + m_->bext ()->umid_ = umid; +} + +quint16 BWFFile::bext_loudness_value () const +{ + if (!m_->bext ()) return 0; + return m_->bext ()->loudness_value_; +} + +void BWFFile::bext_loudness_value (quint16 value) +{ + m_->header_dirty_ = true; + m_->bext ()->loudness_value_ = value; +} + +quint16 BWFFile::bext_loudness_range () const +{ + if (!m_->bext ()) return 0; + return m_->bext ()->loudness_range_; +} + +void BWFFile::bext_loudness_range (quint16 range) +{ + m_->header_dirty_ = true; + m_->bext ()->loudness_range_ = range; +} + +quint16 BWFFile::bext_max_true_peak_level () const +{ + if (!m_->bext ()) return 0; + return m_->bext ()->max_true_peak_level_; +} + +void BWFFile::bext_max_true_peak_level (quint16 level) +{ + m_->header_dirty_ = true; + m_->bext ()->max_true_peak_level_ = level; +} + +quint16 BWFFile::bext_max_momentary_loudness () const +{ + if (!m_->bext ()) return 0; + return m_->bext ()->max_momentary_loudness_; +} + +void BWFFile::bext_max_momentary_loudness (quint16 loudness) +{ + m_->header_dirty_ = true; + m_->bext ()->max_momentary_loudness_ = loudness; +} + +quint16 BWFFile::bext_max_short_term_loudness () const +{ + if (!m_->bext ()) return 0; + return m_->bext ()->max_short_term_loudness_; +} + +void BWFFile::bext_max_short_term_loudness (quint16 loudness) +{ + m_->header_dirty_ = true; + m_->bext ()->max_short_term_loudness_ = loudness; +} + +QByteArray BWFFile::bext_coding_history () const +{ + if (size_t (m_->bext_.size ()) <= sizeof (BroadcastAudioExtension)) return {}; + return QByteArray::fromRawData (m_->bext ()->coding_history_, + m_->bext_.size () - sizeof (BroadcastAudioExtension)); +} + +void BWFFile::bext_coding_history (QByteArray const& text) +{ + m_->header_dirty_ = true; + m_->bext (); // ensure we have a correctly + // initialized m_->bext_ + auto length = std::min (strlen (text.constData ()), size_t (text.size ())); + m_->bext_.resize (sizeof (BroadcastAudioExtension) + length); + std::strncpy (m_->bext ()->coding_history_, text.constData (), length); +} + + +bool BWFFile::reset () +{ + if (m_->file_.isOpen ()) + { + m_->info_dictionary_.clear (); + m_->bext_.clear (); + auto size = m_->data_size_ < 0 ? m_->file_.size () - m_->header_length_ : m_->data_size_; + m_->data_size_ = size; + if (m_->header_length_ > 3 * sizeof (Desc) + 4 + sizeof (FormatChunk)) + { + // we need to move the data down + auto old_pos = m_->header_length_; + m_->write_header (m_->format_); + auto new_pos = m_->header_length_; + QByteArray buffer; + while (size) + { + m_->file_.seek (old_pos); + buffer = m_->file_.read (std::min (size, qint64 (32768))); + m_->file_.seek (new_pos); + m_->file_.write (buffer); + new_pos += buffer.size (); + old_pos += buffer.size (); + size -= buffer.size (); + } + } + m_->file_.resize (m_->header_length_ + m_->data_size_); + m_->header_dirty_ = true; + } return QIODevice::reset (); } -bool WavFile::isSequential () const +qint64 BWFFile::size () const { - return file_.isSequential (); + return m_->data_size_ < 0 ? m_->file_.size () - m_->header_length_ : m_->data_size_; } -void WavFile::close () +bool BWFFile::isSequential () const +{ + return m_->file_.isSequential (); +} + +void BWFFile::close () { QIODevice::close (); - file_.close (); + if (m_->header_dirty_ || m_->data_size_ < 0) m_->update_header (); + m_->file_.close (); } -bool WavFile::seek (qint64 pos) +bool BWFFile::seek (qint64 pos) { if (pos < 0) return false; QIODevice::seek (pos); - return file_.seek (pos + header_length_); + return m_->file_.seek (pos + m_->header_length_); } -qint64 WavFile::readData (char * data, qint64 max_size) +qint64 BWFFile::readData (char * data, qint64 max_size) { - return file_.read (data, max_size); + return m_->file_.read (data, max_size); } -qint64 WavFile::writeData (char const* data, qint64 max_size) +qint64 BWFFile::writeData (char const* data, qint64 max_size) { - auto bytes = file_.write (data, max_size); - if (bytes > 0 && atEnd ()) header_dirty_ = true; + auto bytes = m_->file_.write (data, max_size); + if (bytes > 0 && atEnd ()) + { + m_->header_dirty_ = true; + m_->data_size_ = -1; + } return bytes; } + +// forward to QFile +bool BWFFile::copy (QString const& new_name) +{ + close (); + return m_->file_.copy (new_name); +} + +bool BWFFile::exists () const {return m_->file_.exists ();} + +bool BWFFile::link (QString const& link_name) {return m_->file_.link (link_name);} + +bool BWFFile::remove () +{ + close (); + return m_->file_.remove (); +} + +bool BWFFile::rename (QString const& new_name) +{ + close (); + return m_->file_.rename (new_name); +} + +void BWFFile::setFileName (QString const& name) {m_->file_.setFileName (name);} + +QString BWFFile::symLinkTarget () const {return m_->file_.symLinkTarget ();} + +QString BWFFile::fileName () const {return m_->file_.fileName ();} + +auto BWFFile::permissions () const -> Permissions {return m_->file_.permissions ();} + +bool BWFFile::resize (qint64 new_size) +{ + auto size = m_->file_.size (); + if (pos () > new_size) seek (new_size); + auto result = m_->file_.resize (m_->header_length_ + new_size); + if (m_->data_size_ >= 0) + { + // set any fresh bytes to zero + auto end_of_data = m_->header_length_ + m_->data_size_; + auto length = std::min (size - end_of_data, m_->file_.size () - end_of_data); + if (length > 0) + { + auto position = m_->file_.pos (); + m_->file_.seek (m_->header_length_ + m_->data_size_); + m_->file_.write (QByteArray {int (length), '\0'}); + m_->file_.seek (position); + } + m_->data_size_ = -1; + } + m_->header_dirty_ = true; + return result; +} + +bool BWFFile::setPermissions (Permissions permissions) {return m_->file_.setPermissions (permissions);} + +auto BWFFile::error () const -> FileError {return m_->file_.error ();} + +bool BWFFile::flush () {return m_->file_.flush ();} + +int BWFFile::handle () const {return m_->file_.handle ();} + +uchar * BWFFile::map (qint64 offset, qint64 size, MemoryMapFlags flags) +{ + return m_->file_.map (offset + m_->header_length_, size, flags); +} + +bool BWFFile::unmap (uchar * address) {return m_->file_.unmap (address);} + +void BWFFile::unsetError () {m_->file_.unsetError ();} diff --git a/Audio/WavFile.hpp b/Audio/WavFile.hpp index bfba00f46..513f52c17 100644 --- a/Audio/WavFile.hpp +++ b/Audio/WavFile.hpp @@ -1,17 +1,61 @@ -#ifndef WSV_FILE_HPP__ -#define WSV_FILE_HPP__ +#ifndef BWF_FILE_HPP__ +#define BWF_FILE_HPP__ #include #include -#include #include #include +#include "pimpl_h.hpp" + class QObject; class QString; +class QAudioFormat; -class WavFile final +// +// BWFFile - Broadcast Wave Format File (a.k.a. WAV file) +// +// The BWF file format is a backward compatible variation of the +// Microsoft WAV file format. It contains an extra chunk with id +// 'bext' that contains metadata defined by the EBU in: +// +// https://tech.ebu.ch/docs/tech/tech3285.pdf +// +// Also relevant is the recommendation document: +// +// https://tech.ebu.ch/docs/r/r098.pdf +// +// which suggests a format to the free text coding history field. +// +// This class also supports the LIST-INFO chunk type which also allows +// metadata to be added to a WAV file, the defined INFO tag ids are +// documented here: +// +// http://bwfmetaedit.sourceforge.net/listinfo.html +// +// These ids are not enforced but they are recommended as most +// operating systems and audio applications recognize some or more of +// them. Notably Microsoft Windows is not one of the operating systems +// that does :( In fact there seems to be no documented metadata +// tagging format that Windows Explorer recognizes. +// +// Changes to the 'bext' fields and the LIST-INFO dictionary may be +// made right up until the file is closed as the relevant chunks are +// saved to the end of the file after the end of the sample data. +// +// This class emulates the QFile class, in fact it uses a QFile object +// instance internally and forwards many of its operations directly to +// it. +// +// BWFFile is a QIODevice subclass and the implementation provides +// access to the audio sample data contained in the BWF file as if +// only that data were in the file. I.e. the first sample is at file +// offset zero and the size of the file is the size of the sample +// data. The headers, trailers and metadata are hidden but can be +// accessed by the operations below. +// +class BWFFile : public QIODevice { Q_OBJECT @@ -21,46 +65,136 @@ public: using FileError = QFile::FileError; using MemoryMapFlags = QFile::MemoryMapFlags; using InfoDictionary = QMap, QByteArray>; + using UMID = std::array; + + explicit BWFFile (QAudioFormat const&, QObject * parent = nullptr); + explicit BWFFile (QAudioFormat const&, QString const& name, + QObject * parent = nullptr); + + // The InfoDictionary should contain valid WAV format LIST-INFO + // identifiers as keys, a list of them can be found here: + // + // http://bwfmetaedit.sourceforge.net/listinfo.html + // + // For files opened for ReadOnly access the dictionary is not + // written to the file. For files opened ReadWrite, any existing + // LIST-INFO tags will be merged into the dictionary when the file + // is opened and if the file is modified the merged dictionary will + // be written back to the file. + // + // Note that the sample data may no be in the native endian, it is + // the callers responsibility to do any required endian + // conversions. The internal data is always in native endian with + // conversions being handled automatically. Use the BWF::format() + // operation to access the format including the + // QAudioFormat::byteOrder() operation to determine the data byte + // ordering. + // + explicit BWFFile (QAudioFormat const&, QString const& name, + InfoDictionary const&, QObject * parent = nullptr); + + ~BWFFile (); + QAudioFormat const& format () const; + InfoDictionary& list_info (); + + // + // Broadcast Audio Extension fields + // + // If any of these modifiers are called then a "bext" chunk will be + // written to the file if the file is writeable and the sample data + // is modified. + // + enum class BextVersion : quint16 {v_0, v_1, v_2}; + BextVersion bext_version () const; + void bext_version (BextVersion = BextVersion::v_2); + + QByteArray bext_description () const; + void bext_description (QByteArray const&); // max 256 bytes + + QByteArray bext_originator () const; + void bext_originator (QByteArray const&); // max 32 bytes + + QByteArray bext_originator_reference () const; + void bext_originator_reference (QByteArray const&); // max 32 bytes + + QDateTime bext_origination_date_time () const; + void bext_origination_date_time (QDateTime const&); // 1s resolution + + quint64 bext_time_reference () const; + void bext_time_reference (quint64); // samples since midnight at start + + UMID bext_umid () const; // bext version >= 1 only + void bext_umid (UMID const&); + + quint16 bext_loudness_value () const; + void bext_loudness_value (quint16); // bext version >= 2 only + + quint16 bext_loudness_range () const; + void bext_loudness_range (quint16); // bext version >= 2 only + + quint16 bext_max_true_peak_level () const; + void bext_max_true_peak_level (quint16); // bext version >= 2 only + + quint16 bext_max_momentary_loudness () const; + void bext_max_momentary_loudness (quint16); // bext version >= 2 only + + quint16 bext_max_short_term_loudness () const; + void bext_max_short_term_loudness (quint16); // bext version >= 2 only + + QByteArray bext_coding_history () const; + void bext_coding_history (QByteArray const&); // See EBU R 98 - 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); + bool exists () const; + bool link (QString const& link_name); + bool remove (); + bool rename (QString const& new_name); + void setFileName (QString const& name); + QString symLinkTarget () const; + QString fileName () const; + Permissions permissions () const; - // 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 ();} + // Resize is of the sample data portion, header and trailer chunks + // are excess to the given size + bool resize (qint64 new_size); + + bool setPermissions (Permissions permissions); + FileError error () const; + bool flush (); + int handle () const; + + // The mapping offset is relative to the start of the sample data + uchar * map (qint64 offset, qint64 size, + MemoryMapFlags = QFile::NoOptions); + bool unmap (uchar * address); + + void unsetError (); + + + // + // QIODevice implementation + // + + // The size returned is of the sample data only, header and trailer + // chunks are hidden and handled internally + qint64 size () const override; - // QIODevice overrides bool isSequential () const override; + + // The reset operation clears the 'bext' and LIST-INFO as if they + // were never supplied. If the file is writable the 'bext' and + // LIST-INFO chunks will not be written making the resulting file a + // lowest common denominator WAV file. bool reset () override; + + // Seek offsets are relative to the start of the sample data bool seek (qint64) override; + void close () override; protected: @@ -68,16 +202,8 @@ protected: 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_; + class impl; + pimpl m_; }; #endif diff --git a/lib/jt65_decode.f90 b/lib/jt65_decode.f90 index 562a46b31..1ab84662c 100644 --- a/lib/jt65_decode.f90 +++ b/lib/jt65_decode.f90 @@ -1,229 +1,229 @@ -module jt65_decode - - type :: jt65_decoder - procedure(jt65_decode_callback), pointer :: callback => null() - contains - procedure :: decode - end type jt65_decoder - - ! - ! Callback function to be called with each decode - ! - abstract interface - subroutine jt65_decode_callback (this, utc, sync, snr, dt, freq, drift, & - decoded, ft, qual, candidates, tries, total_min, hard_min, aggression) - import jt65_decoder - implicit none - class(jt65_decoder), intent(inout) :: this - integer, intent(in) :: utc - real, intent(in) :: sync - integer, intent(in) :: snr - real, intent(in) :: dt - integer, intent(in) :: freq - integer, intent(in) :: drift - character(len=22), intent(in) :: decoded - integer, intent(in) :: ft - integer, intent(in) :: qual - integer, intent(in) :: candidates - integer, intent(in) :: tries - integer, intent(in) :: total_min - integer, intent(in) :: hard_min - integer, intent(in) :: aggression - end subroutine jt65_decode_callback - end interface - -contains - - subroutine decode(this,callback,dd0,npts,newdat,nutc,nf1,nf2,nfqso,ntol,nsubmode, & - minsync,nagain,n2pass,nrobust,ntrials,naggressive,ndepth, & - mycall,hiscall,hisgrid,nexp_decode) - - ! Process dd0() data to find and decode JT65 signals. - - use timer_module, only: timer - - include 'constants.f90' - parameter (NSZ=3413,NZMAX=60*12000) - parameter (NFFT=1000) - - class(jt65_decoder), intent(inout) :: this - procedure(jt65_decode_callback) :: callback - real, intent(in) :: dd0(NZMAX) - integer, intent(in) :: npts, nutc, nf1, nf2, nfqso, ntol & - , nsubmode, minsync, n2pass, ntrials, naggressive, ndepth & - , nexp_decode - logical, intent(in) :: newdat, nagain, nrobust - character(len=12), intent(in) :: mycall, hiscall - character(len=6), intent(in) :: hisgrid - - real dd(NZMAX) - real ss(322,NSZ) - real savg(NSZ) - real a(5) - character*22 decoded,decoded0 - type candidate - real freq - real dt - real sync - end type candidate - type(candidate) ca(300) - type accepted_decode - real freq - real dt - real sync - character*22 decoded - end type accepted_decode - type(accepted_decode) dec(50) - logical :: first_time, robust - - integer h0(0:11),d0(0:11),ne(0:11) - real r0(0:11) - common/decstats/ntry65a,ntry65b,n65a,n65b,num9,numfano - common/steve/thresh0 - common/test000/ncandidates,nhard_min,nsoft_min,nera_best,nrtt1000, & - ntotal_min,ntry,nq1000,npp1 !### TEST ONLY ### - -! 0 1 2 3 4 5 6 7 8 9 10 11 - data h0/41,42,43,43,44,45,46,47,48,48,49,49/ - data d0/71,72,73,74,76,77,78,80,81,82,83,83/ - -! 0 1 2 3 4 5 6 7 8 9 10 11 - data r0/0.70,0.72,0.74,0.76,0.78,0.80,0.82,0.84,0.86,0.88,0.90,0.90/ - save - - this%callback => callback - first_time=newdat - robust=nrobust - dd=dd0 - ndecoded=0 - do ipass=1,n2pass ! 2-pass decoding loop - first_time=.true. - if(ipass.eq.1) then !first-pass parameters - thresh0=2.5 - nsubtract=1 - elseif( ipass.eq.2 ) then !second-pass parameters - thresh0=2.5 - nsubtract=0 - endif - if(n2pass.lt.2) nsubtract=0 - - ! if(newdat) then - call timer('symsp65 ',0) - ss=0. - call symspec65(dd,npts,ss,nhsym,savg) !Get normalized symbol spectra - call timer('symsp65 ',1) - ! endif - nfa=nf1 - nfb=nf2 - if(naggressive.gt.0 .and. ntol.lt.1000) then - nfa=max(200,nfqso-ntol) - nfb=min(4000,nfqso+ntol) - thresh0=1.0 - endif - - ! robust = .false.: use float ccf. Only if ncand>50 fall back to robust (1-bit) ccf - ! robust = .true. : use only robust (1-bit) ccf - ncand=0 - if(.not.robust) then - call timer('sync65 ',0) - call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,0) - call timer('sync65 ',1) - endif - if(ncand.gt.50) robust=.true. - if(robust) then - ncand=0 - call timer('sync65 ',0) - call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,1) - call timer('sync65 ',1) - endif - - call fqso_first(nfqso,ntol,ca,ncand) - - nvec=ntrials - if(ncand.gt.75) then - ! write(*,*) 'Pass ',ipass,' ncandidates too large ',ncand - nvec=100 - endif - - df=12000.0/NFFT !df = 12000.0/8192 = 1.465 Hz - mode65=2**nsubmode - nflip=1 !### temporary ### - nqd=0 - decoded0="" - freq0=0. - - do icand=1,ncand - freq=ca(icand)%freq - dtx=ca(icand)%dt - sync1=ca(icand)%sync - if(ipass.eq.1) ntry65a=ntry65a + 1 - if(ipass.eq.2) ntry65b=ntry65b + 1 - call timer('decod65a',0) - call decode65a(dd,npts,first_time,nqd,freq,nflip,mode65,nvec, & - naggressive,ndepth,mycall,hiscall,hisgrid,nexp_decode, & - sync2,a,dtx,nft,qual,nhist,decoded) - call timer('decod65a',1) - n=naggressive - rtt=0.001*nrtt1000 - if(nft.lt.2) then - if(nhard_min.gt.50) cycle - if(nhard_min.gt.h0(n)) cycle - if(ntotal_min.gt.d0(n)) cycle - if(rtt.gt.r0(n)) cycle - endif - -! !### Suppress false decodes in crowded HF bands ### -! if(naggressive.eq.0 .and. ntrials.le.10000) then -! if(ntry.eq.ntrials) then -! if(nhard_min.ge.42 .or. ntotal_min.ge.71) cycle -! endif -! endif - if(decoded.eq.decoded0 .and. abs(freq-freq0).lt. 3.0 .and. & - minsync.ge.0) cycle !Don't display dupes - if(decoded.ne.' ' .or. minsync.lt.0) then - if( nsubtract .eq. 1 ) then - call timer('subtr65 ',0) - call subtract65(dd,npts,freq,dtx) - call timer('subtr65 ',1) - endif - nfreq=nint(freq+a(1)) - ndrift=nint(2.0*a(2)) - s2db=10.0*log10(sync2) - 35 !### empirical ### - nsnr=nint(s2db) - if(nsnr.lt.-30) nsnr=-30 - if(nsnr.gt.-1) nsnr=-1 - - ndupe=0 ! de-dedupe - do i=1, ndecoded - if(decoded==dec(i)%decoded) then - ndupe=1 - exit - endif - enddo - if(ndupe.ne.1 .or. minsync.lt.0) then - if(ipass.eq.1) n65a=n65a + 1 - if(ipass.eq.2) n65b=n65b + 1 - ndecoded=ndecoded+1 - dec(ndecoded)%freq=freq+a(1) - dec(ndecoded)%dt=dtx - dec(ndecoded)%sync=sync2 - dec(ndecoded)%decoded=decoded - nqual=min(qual,9999.0) - ! if(nqual.gt.10) nqual=10 - if (associated(this%callback)) then - call this%callback(nutc,sync1,nsnr,dtx-1.0,nfreq,ndrift,decoded & - ,nft,nqual,ncandidates,ntry,ntotal_min,nhard_min,naggressive) - end if - endif - decoded0=decoded - freq0=freq - if(decoded0.eq.' ') decoded0='*' - endif - enddo !candidate loop - if(ndecoded.lt.1) exit - enddo !two-pass loop - - return - end subroutine decode - -end module jt65_decode +module jt65_decode + + integer, parameter :: NSZ=3413, NZMAX=60*12000, NFFT=1000 + + type :: jt65_decoder + procedure(jt65_decode_callback), pointer :: callback => null() + contains + procedure :: decode + end type jt65_decoder + + ! + ! Callback function to be called with each decode + ! + abstract interface + subroutine jt65_decode_callback (this, utc, sync, snr, dt, freq, drift, & + decoded, ft, qual, candidates, tries, total_min, hard_min, aggression) + import jt65_decoder + implicit none + class(jt65_decoder), intent(inout) :: this + integer, intent(in) :: utc + real, intent(in) :: sync + integer, intent(in) :: snr + real, intent(in) :: dt + integer, intent(in) :: freq + integer, intent(in) :: drift + character(len=22), intent(in) :: decoded + integer, intent(in) :: ft + integer, intent(in) :: qual + integer, intent(in) :: candidates + integer, intent(in) :: tries + integer, intent(in) :: total_min + integer, intent(in) :: hard_min + integer, intent(in) :: aggression + end subroutine jt65_decode_callback + end interface + +contains + + subroutine decode(this,callback,dd0,npts,newdat,nutc,nf1,nf2,nfqso,ntol,nsubmode, & + minsync,nagain,n2pass,nrobust,ntrials,naggressive,ndepth, & + mycall,hiscall,hisgrid,nexp_decode) + + ! Process dd0() data to find and decode JT65 signals. + + use timer_module, only: timer + + include 'constants.f90' + + class(jt65_decoder), intent(inout) :: this + procedure(jt65_decode_callback) :: callback + real, intent(in) :: dd0(NZMAX) + integer, intent(in) :: npts, nutc, nf1, nf2, nfqso, ntol & + , nsubmode, minsync, n2pass, ntrials, naggressive, ndepth & + , nexp_decode + logical, intent(in) :: newdat, nagain, nrobust + character(len=12), intent(in) :: mycall, hiscall + character(len=6), intent(in) :: hisgrid + + real dd(NZMAX) + real ss(322,NSZ) + real savg(NSZ) + real a(5) + character*22 decoded,decoded0 + type candidate + real freq + real dt + real sync + end type candidate + type(candidate) ca(300) + type accepted_decode + real freq + real dt + real sync + character*22 decoded + end type accepted_decode + type(accepted_decode) dec(50) + logical :: first_time, robust + + integer h0(0:11),d0(0:11),ne(0:11) + real r0(0:11) + common/decstats/ntry65a,ntry65b,n65a,n65b,num9,numfano + common/steve/thresh0 + common/test000/ncandidates,nhard_min,nsoft_min,nera_best,nrtt1000, & + ntotal_min,ntry,nq1000,npp1 !### TEST ONLY ### + +! 0 1 2 3 4 5 6 7 8 9 10 11 + data h0/41,42,43,43,44,45,46,47,48,48,49,49/ + data d0/71,72,73,74,76,77,78,80,81,82,83,83/ + +! 0 1 2 3 4 5 6 7 8 9 10 11 + data r0/0.70,0.72,0.74,0.76,0.78,0.80,0.82,0.84,0.86,0.88,0.90,0.90/ + save + + this%callback => callback + first_time=newdat + robust=nrobust + dd=dd0 + ndecoded=0 + do ipass=1,n2pass ! 2-pass decoding loop + first_time=.true. + if(ipass.eq.1) then !first-pass parameters + thresh0=2.5 + nsubtract=1 + elseif( ipass.eq.2 ) then !second-pass parameters + thresh0=2.5 + nsubtract=0 + endif + if(n2pass.lt.2) nsubtract=0 + + ! if(newdat) then + call timer('symsp65 ',0) + ss=0. + call symspec65(dd,npts,ss,nhsym,savg) !Get normalized symbol spectra + call timer('symsp65 ',1) + ! endif + nfa=nf1 + nfb=nf2 + if(naggressive.gt.0 .and. ntol.lt.1000) then + nfa=max(200,nfqso-ntol) + nfb=min(4000,nfqso+ntol) + thresh0=1.0 + endif + + ! robust = .false.: use float ccf. Only if ncand>50 fall back to robust (1-bit) ccf + ! robust = .true. : use only robust (1-bit) ccf + ncand=0 + if(.not.robust) then + call timer('sync65 ',0) + call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,0) + call timer('sync65 ',1) + endif + if(ncand.gt.50) robust=.true. + if(robust) then + ncand=0 + call timer('sync65 ',0) + call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,1) + call timer('sync65 ',1) + endif + + call fqso_first(nfqso,ntol,ca,ncand) + + nvec=ntrials + if(ncand.gt.75) then + ! write(*,*) 'Pass ',ipass,' ncandidates too large ',ncand + nvec=100 + endif + + df=12000.0/NFFT !df = 12000.0/8192 = 1.465 Hz + mode65=2**nsubmode + nflip=1 !### temporary ### + nqd=0 + decoded0="" + freq0=0. + + do icand=1,ncand + freq=ca(icand)%freq + dtx=ca(icand)%dt + sync1=ca(icand)%sync + if(ipass.eq.1) ntry65a=ntry65a + 1 + if(ipass.eq.2) ntry65b=ntry65b + 1 + call timer('decod65a',0) + call decode65a(dd,npts,first_time,nqd,freq,nflip,mode65,nvec, & + naggressive,ndepth,mycall,hiscall,hisgrid,nexp_decode, & + sync2,a,dtx,nft,qual,nhist,decoded) + call timer('decod65a',1) + n=naggressive + rtt=0.001*nrtt1000 + if(nft.lt.2) then + if(nhard_min.gt.50) cycle + if(nhard_min.gt.h0(n)) cycle + if(ntotal_min.gt.d0(n)) cycle + if(rtt.gt.r0(n)) cycle + endif + +! !### Suppress false decodes in crowded HF bands ### +! if(naggressive.eq.0 .and. ntrials.le.10000) then +! if(ntry.eq.ntrials) then +! if(nhard_min.ge.42 .or. ntotal_min.ge.71) cycle +! endif +! endif + if(decoded.eq.decoded0 .and. abs(freq-freq0).lt. 3.0 .and. & + minsync.ge.0) cycle !Don't display dupes + if(decoded.ne.' ' .or. minsync.lt.0) then + if( nsubtract .eq. 1 ) then + call timer('subtr65 ',0) + call subtract65(dd,npts,freq,dtx) + call timer('subtr65 ',1) + endif + nfreq=nint(freq+a(1)) + ndrift=nint(2.0*a(2)) + s2db=10.0*log10(sync2) - 35 !### empirical ### + nsnr=nint(s2db) + if(nsnr.lt.-30) nsnr=-30 + if(nsnr.gt.-1) nsnr=-1 + + ndupe=0 ! de-dedupe + do i=1, ndecoded + if(decoded==dec(i)%decoded) then + ndupe=1 + exit + endif + enddo + if(ndupe.ne.1 .or. minsync.lt.0) then + if(ipass.eq.1) n65a=n65a + 1 + if(ipass.eq.2) n65b=n65b + 1 + ndecoded=ndecoded+1 + dec(ndecoded)%freq=freq+a(1) + dec(ndecoded)%dt=dtx + dec(ndecoded)%sync=sync2 + dec(ndecoded)%decoded=decoded + nqual=min(qual,9999.0) + ! if(nqual.gt.10) nqual=10 + if (associated(this%callback)) then + call this%callback(nutc,sync1,nsnr,dtx-1.0,nfreq,ndrift,decoded & + ,nft,nqual,ncandidates,ntry,ntotal_min,nhard_min,naggressive) + end if + endif + decoded0=decoded + freq0=freq + if(decoded0.eq.' ') decoded0='*' + endif + enddo !candidate loop + if(ndecoded.lt.1) exit + enddo !two-pass loop + + return + end subroutine decode + +end module jt65_decode diff --git a/mainwindow.cpp b/mainwindow.cpp index a4e0619c8..870c89a4b 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -35,7 +35,6 @@ #include "messageaveraging.h" #include "widegraph.h" #include "sleep.h" -#include "getfile.h" #include "logqso.h" #include "Radio.hpp" #include "Bands.hpp" @@ -48,6 +47,7 @@ #include "signalmeter.h" #include "HelpTextWindow.hpp" #include "SampleDownloader.hpp" +#include "Audio/WavFile.hpp" #include "ui_mainwindow.h" #include "moc_mainwindow.cpp" @@ -98,6 +98,7 @@ extern "C" { void fast_decode_(short id2[], int narg[], char msg[], int len); void degrade_snr_(short d2[], int* n, float* db); + void wav12_(short d2[], short d1[], int* nbytes, short* nbitsam2); } int volatile itone[NUM_ISCAT_SYMBOLS]; //Audio tones for all Tx symbols @@ -614,13 +615,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme m_wideGraph->setMode(m_mode); m_wideGraph->setModeTx(m_modeTx); - future1 = new QFuture; - watcher1 = new QFutureWatcher; - connect(watcher1, SIGNAL(finished()),this,SLOT(diskDat())); - - future2 = new QFuture; - watcher2 = new QFutureWatcher; - connect(watcher2, SIGNAL(finished()),this,SLOT(diskWriteFinished())); + connect (&m_wav_future_watcher, &QFutureWatcher::finished, this, &MainWindow::diskDat); future3 = new QFuture; watcher3 = new QFutureWatcher; @@ -961,8 +956,9 @@ void MainWindow::dataSink(qint64 frames) t2.sprintf("%2.2d%2.2d",ihr,imin); m_fileToSave.clear (); m_fname = m_config.save_directory ().absoluteFilePath (t.date().toString("yyMMdd") + "_" + t2); - *future2 = QtConcurrent::run(savewav, m_fname + ".wav", m_TRperiod); - watcher2->setFuture(*future2); + // the following is potential a threading hazard - not a good + // idea to pass pointer to be processed in another thread + QtConcurrent::run(this, &MainWindow::save_wave_file, m_fname + ".wav", &dec_data.d2[0], m_TRperiod); if (m_mode.mid (0,4) == "WSPR") { m_c2name = m_fname + ".c2"; @@ -1006,6 +1002,36 @@ void MainWindow::dataSink(qint64 frames) } } +void MainWindow::save_wave_file (QString const& name, short const * data, int seconds) const +{ + QAudioFormat format; + format.setCodec ("audio/pcm"); + format.setSampleRate (12000); + format.setChannelCount (1); + format.setSampleSize (16); + format.setSampleType (QAudioFormat::SignedInt); + auto source = QString {"%1, %2"}.arg (m_config.my_callsign ()).arg (m_config.my_grid ()); + auto comment = QString {"Mode=%1%2, Freq=%3%4"} + .arg (m_mode) + .arg (QString {m_mode.contains ('J') && !m_mode.contains ('+') + ? QString {", Sub Mode="} + QChar {'A' + m_nSubMode} + : QString {}}) + .arg (Radio::frequency_MHz_string (m_dialFreq)) + .arg (QString {!m_mode.contains ("WSPR") ? QString {", DXCall=%1, DXGrid=%2"} + .arg (m_hisCall) + .arg (m_hisGrid).toLocal8Bit () : ""}); + BWFFile::InfoDictionary list_info { + {{'I','S','R','C'}, source.toLocal8Bit ()}, + {{'I','S','F','T'}, program_title (revision ()).simplified ().toLocal8Bit ()}, + {{'I','C','R','D'}, QDateTime::currentDateTime () + .toString ("yyyy-MM-ddTHH:mm:ss.zzzZ").toLocal8Bit ()}, + {{'I','C','M','T'}, comment.toLocal8Bit ()}, + }; + BWFFile wav {format, name, list_info}; + wav.open (BWFFile::WriteOnly); + wav.write (reinterpret_cast (data), sizeof (short) * seconds * format.sampleRate ()); +} + //-------------------------------------------------------------- fastSink() void MainWindow::fastSink(qint64 frames) { @@ -1062,8 +1088,9 @@ void MainWindow::fastSink(qint64 frames) } if(!m_diskData and (m_saveAll or m_saveDecoded) and m_fname != "" and !decodeEarly) { - *future2 = QtConcurrent::run(savewav, m_fname, m_TRperiod); - watcher2->setFuture(*future2); + // the following is potential a threading hazard - not a good + // idea to pass pointer to be processed in another thread + QtConcurrent::run (this, &MainWindow::save_wave_file, m_fname, &dec_data.d2[0], m_TRperiod); m_fileToKill=m_fname; killFileTimer->start (3*1000*m_TRperiod/4); //Kill 3/4 period from now } @@ -1544,7 +1571,7 @@ void MainWindow::on_actionOpen_triggered() //Open File QString fname; fname=QFileDialog::getOpenFileName(this, "Open File", m_path, "WSJT Files (*.wav)"); - if(fname != "") { + if(!fname.isEmpty ()) { m_path=fname; int i1=fname.lastIndexOf("/"); QString baseName=fname.mid(i1+1); @@ -1552,11 +1579,46 @@ void MainWindow::on_actionOpen_triggered() //Open File tx_status_label->setText(" " + baseName + " "); on_stopButton_clicked(); m_diskData=true; - *future1 = QtConcurrent::run(getfile, fname, m_TRperiod); - watcher1->setFuture(*future1); // call diskDat() when done + read_wav_file (fname); } } +void MainWindow::read_wav_file (QString const& fname) +{ + m_wav_future = QtConcurrent::run ([this, fname] { + auto basename = fname.mid (fname.lastIndexOf ('/') + 1); + auto pos = fname.indexOf (".wav", 0, Qt::CaseInsensitive); + // global variables and threads do not mix well, this needs changing + dec_data.params.nutc = 0; + if (pos > 0) + { + if (pos == fname.indexOf ('_', -11) + 7) + { + dec_data.params.nutc = fname.mid (pos - 6, 6).toInt (); + } + else + { + dec_data.params.nutc = 100 * fname.mid (pos - 4, 4).toInt (); + } + } + BWFFile file {QAudioFormat {}, fname}; + file.open (BWFFile::ReadOnly); + auto ntps = std::min (m_TRperiod * 12000, 120 * 12000); + auto bytes_per_frame = file.format ().bytesPerFrame (); + int n = file.read (reinterpret_cast (dec_data.d2), + std::min (qint64 (bytes_per_frame * ntps), file.size ())); + std::memset (dec_data.d2 + n, 0, bytes_per_frame * ntps - n); + if (11025 == file.format ().sampleRate ()) + { + auto sample_size = static_cast (file.format ().sampleSize ()); + wav12_ (dec_data.d2, dec_data.d2, &n, &sample_size); + } + dec_data.params.kin = n; + dec_data.params.newdat = 1; + }); + m_wav_future_watcher.setFuture(m_wav_future); // call diskDat() when done +} + void MainWindow::on_actionOpen_next_in_directory_triggered() //Open Next { monitor (false); @@ -1577,9 +1639,7 @@ void MainWindow::on_actionOpen_next_in_directory_triggered() //Open Next tx_status_label->setStyleSheet("QLabel{background-color: #99ffff}"); tx_status_label->setText(" " + baseName + " "); m_diskData=true; - *future1 = QtConcurrent::run(getfile, fname, m_TRperiod); - watcher1->setFuture(*future1); - return; + read_wav_file (fname); } } } @@ -1609,10 +1669,6 @@ void MainWindow::diskDat() //diskDat() } } -void MainWindow::diskWriteFinished() //diskWriteFinished -{ -} - //Delete ../save/*.wav void MainWindow::on_actionDelete_all_wav_files_in_SaveDir_triggered() { diff --git a/mainwindow.h b/mainwindow.h index 97bef15cf..838a97701 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -93,7 +93,6 @@ public slots: void dataSink(qint64 frames); void fastSink(qint64 frames); void diskDat(); - void diskWriteFinished(); void freezeDecode(int n); void guiUpdate(); void doubleClickOnCall(bool shift, bool ctrl); @@ -436,12 +435,10 @@ private: QMessageBox msgBox0; - QFuture* future1; - QFuture* future2; + QFuture m_wav_future; QFuture* future3; - QFutureWatcher* watcher1; - QFutureWatcher* watcher2; - QFutureWatcher* watcher3; + QFutureWatcher m_wav_future_watcher; + QFutureWatcher * watcher3; QProcess proc_jt9; QProcess p1; @@ -559,15 +556,14 @@ private: QString WSPR_hhmm(int n); void fast_config(bool b); void CQRxFreq(); + void save_wave_file (QString const& name, short const * data, int seconds) const; + void read_wav_file (QString const& fname); }; -extern void getfile(QString fname, int ntrperiod); -extern void savewav(QString fname, int ntrperiod); extern int killbyname(const char* progName); extern void getDev(int* numDevices,char hostAPI_DeviceName[][50], int minChan[], int maxChan[], int minSpeed[], int maxSpeed[]); -extern int ptt(int nport, int ntx, int* iptt, int* nopen); extern int next_tx_state(int pctx); #endif // MAINWINDOW_H