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
This commit is contained in:
Bill Somerville 2016-01-11 15:00:43 +00:00
parent fee89ecf67
commit 4e6de783b0
5 changed files with 1098 additions and 462 deletions

View File

@ -6,12 +6,19 @@
#include <qendian.h>
#include <QAudioFormat>
#include <QDebug>
#include <QDateTime>
#include <QDate>
#include <QTime>
#include <QString>
#include <QUuid>
#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<quint16> (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<BroadcastAudioExtension const *> (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<BroadcastAudioExtension *> (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<quint32> (wave_desc.size_) : qFromLittleEndian<quint32> (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<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));
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<quint32> (size) : qToLittleEndian<quint32> (size);
desc.set ("data", 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));
if (!bext_.isEmpty ())
{
if (!file_.seek (file_.size ())) return false;
auto size = bext_.size ();
desc.set ("bext", be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (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<BroadcastAudioExtension *> (bext_.data ());
if (be)
{
data->time_reference_low_ = qToBigEndian<quint32> (data->time_reference_low_);
data->time_reference_high_ = qToBigEndian<quint32> (data->time_reference_high_);
switch (static_cast<BextVersion> (data->version_))
{
case BextVersion::v_0:
data->version_ = qToBigEndian<quint32> (data->version_);
default:
data->loudness_value_ = qToBigEndian<quint16> (data->loudness_value_);
data->loudness_range_ = qToBigEndian<quint16> (data->loudness_range_);
data->max_true_peak_level_ = qToBigEndian<quint16> (data->max_true_peak_level_);
data->max_momentary_loudness_ = qToBigEndian<quint16> (data->max_momentary_loudness_);
data->max_short_term_loudness_ = qToBigEndian<quint16> (data->max_short_term_loudness_);
}
}
else
{
data->time_reference_low_ = qToLittleEndian<quint32> (data->time_reference_low_);
data->time_reference_high_ = qToLittleEndian<quint32> (data->time_reference_high_);
switch (static_cast<BextVersion> (data->version_))
{
case BextVersion::v_0:
data->version_ = qToLittleEndian<quint32> (data->version_);
default:
data->loudness_value_ = qToLittleEndian<quint16> (data->loudness_value_);
data->loudness_range_ = qToLittleEndian<quint16> (data->loudness_range_);
data->max_true_peak_level_ = qToLittleEndian<quint16> (data->max_true_peak_level_);
data->max_momentary_loudness_ = qToLittleEndian<quint16> (data->max_momentary_loudness_);
data->max_short_term_loudness_ = qToLittleEndian<quint16> (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<quint32> (len) : qToLittleEndian<quint32> (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<quint32> (size) : qToLittleEndian<quint32> (size));
if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
}
size = file_.size () - sizeof desc;
desc.size_ = be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (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<quint32> (size) : qToLittleEndian<quint32> (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<BextVersion> (m_->bext () ? 0 : m_->bext ()->version_);
}
void BWFFile::bext_version (BextVersion version)
{
m_->header_dirty_ = true;
m_->bext ()->version_ = static_cast<quint16> (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 ();}

View File

@ -1,17 +1,61 @@
#ifndef WSV_FILE_HPP__
#define WSV_FILE_HPP__
#ifndef BWF_FILE_HPP__
#define BWF_FILE_HPP__
#include <array>
#include <QFile>
#include <QAudioFormat>
#include <QMap>
#include <QByteArray>
#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<std::array<char, 4>, QByteArray>;
using UMID = std::array<quint8, 64>;
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<impl> m_;
};
#endif

View File

@ -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

View File

@ -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<void>;
watcher1 = new QFutureWatcher<void>;
connect(watcher1, SIGNAL(finished()),this,SLOT(diskDat()));
future2 = new QFuture<void>;
watcher2 = new QFutureWatcher<void>;
connect(watcher2, SIGNAL(finished()),this,SLOT(diskWriteFinished()));
connect (&m_wav_future_watcher, &QFutureWatcher<void>::finished, this, &MainWindow::diskDat);
future3 = new QFuture<void>;
watcher3 = new QFutureWatcher<void>;
@ -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<char const *> (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<char *> (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<short > (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()
{

View File

@ -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<void>* future1;
QFuture<void>* future2;
QFuture<void> m_wav_future;
QFuture<void>* future3;
QFutureWatcher<void>* watcher1;
QFutureWatcher<void>* watcher2;
QFutureWatcher<void>* watcher3;
QFutureWatcher<void> m_wav_future_watcher;
QFutureWatcher<void> * 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