mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2025-03-22 12:08:43 -04:00
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:
parent
fee89ecf67
commit
4e6de783b0
@ -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 ();}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
100
mainwindow.cpp
100
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<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()
|
||||
{
|
||||
|
14
mainwindow.h
14
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<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
|
||||
|
Loading…
Reference in New Issue
Block a user