diff --git a/Audio/WavFile.cpp b/Audio/WavFile.cpp
index 43e1085cf..116f38f36 100644
--- a/Audio/WavFile.cpp
+++ b/Audio/WavFile.cpp
@@ -6,12 +6,19 @@
 
 #include <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 ();}
diff --git a/Audio/WavFile.hpp b/Audio/WavFile.hpp
index bfba00f46..513f52c17 100644
--- a/Audio/WavFile.hpp
+++ b/Audio/WavFile.hpp
@@ -1,17 +1,61 @@
-#ifndef WSV_FILE_HPP__
-#define WSV_FILE_HPP__
+#ifndef BWF_FILE_HPP__
+#define BWF_FILE_HPP__
 
 #include <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
diff --git a/lib/jt65_decode.f90 b/lib/jt65_decode.f90
index 562a46b31..1ab84662c 100644
--- a/lib/jt65_decode.f90
+++ b/lib/jt65_decode.f90
@@ -1,229 +1,229 @@
-module jt65_decode
-
-  type :: jt65_decoder
-     procedure(jt65_decode_callback), pointer :: callback => null()
-   contains
-     procedure :: decode
-  end type jt65_decoder
-
-  !
-  ! Callback function to be called with each decode
-  !
-  abstract interface
-     subroutine jt65_decode_callback (this, utc, sync, snr, dt, freq, drift,          &
-          decoded, ft, qual, candidates, tries, total_min, hard_min, aggression)
-       import jt65_decoder
-       implicit none
-       class(jt65_decoder), intent(inout) :: this
-       integer, intent(in) :: utc
-       real, intent(in) :: sync
-       integer, intent(in) :: snr
-       real, intent(in) :: dt
-       integer, intent(in) :: freq
-       integer, intent(in) :: drift
-       character(len=22), intent(in) :: decoded
-       integer, intent(in) :: ft
-       integer, intent(in) :: qual
-       integer, intent(in) :: candidates
-       integer, intent(in) :: tries
-       integer, intent(in) :: total_min
-       integer, intent(in) :: hard_min
-       integer, intent(in) :: aggression
-     end subroutine jt65_decode_callback
-  end interface
-
-contains
-
-  subroutine decode(this,callback,dd0,npts,newdat,nutc,nf1,nf2,nfqso,ntol,nsubmode,   &
-       minsync,nagain,n2pass,nrobust,ntrials,naggressive,ndepth,       &
-       mycall,hiscall,hisgrid,nexp_decode)
-
-    !  Process dd0() data to find and decode JT65 signals.
-
-    use timer_module, only: timer
-
-    include 'constants.f90'
-    parameter (NSZ=3413,NZMAX=60*12000)
-    parameter (NFFT=1000)
-
-    class(jt65_decoder), intent(inout) :: this
-    procedure(jt65_decode_callback) :: callback
-    real, intent(in) :: dd0(NZMAX)
-    integer, intent(in) :: npts, nutc, nf1, nf2, nfqso, ntol     &
-         , nsubmode, minsync, n2pass, ntrials, naggressive, ndepth      &
-         , nexp_decode
-    logical, intent(in) :: newdat, nagain, nrobust
-    character(len=12), intent(in) :: mycall, hiscall
-    character(len=6), intent(in) :: hisgrid
-
-    real dd(NZMAX)
-    real ss(322,NSZ)
-    real savg(NSZ)
-    real a(5)
-    character*22 decoded,decoded0
-    type candidate
-       real freq
-       real dt
-       real sync
-    end type candidate
-    type(candidate) ca(300)
-    type accepted_decode
-       real freq
-       real dt
-       real sync
-       character*22 decoded
-    end type accepted_decode
-    type(accepted_decode) dec(50)
-    logical :: first_time, robust
-
-    integer h0(0:11),d0(0:11),ne(0:11)
-    real r0(0:11)
-    common/decstats/ntry65a,ntry65b,n65a,n65b,num9,numfano
-    common/steve/thresh0
-    common/test000/ncandidates,nhard_min,nsoft_min,nera_best,nrtt1000,   &
-         ntotal_min,ntry,nq1000,npp1         !### TEST ONLY ###
-
-!            0  1  2  3  4  5  6  7  8  9 10 11
-    data h0/41,42,43,43,44,45,46,47,48,48,49,49/
-    data d0/71,72,73,74,76,77,78,80,81,82,83,83/
-
-!             0    1    2    3    4    5    6    7    8    9   10   11
-    data r0/0.70,0.72,0.74,0.76,0.78,0.80,0.82,0.84,0.86,0.88,0.90,0.90/
-    save
-
-    this%callback => callback
-    first_time=newdat
-    robust=nrobust
-    dd=dd0
-    ndecoded=0
-    do ipass=1,n2pass                             ! 2-pass decoding loop
-       first_time=.true.
-       if(ipass.eq.1) then                         !first-pass parameters
-          thresh0=2.5
-          nsubtract=1
-       elseif( ipass.eq.2 ) then !second-pass parameters
-          thresh0=2.5
-          nsubtract=0
-       endif
-       if(n2pass.lt.2) nsubtract=0
-
-       !  if(newdat) then
-       call timer('symsp65 ',0)
-       ss=0.
-       call symspec65(dd,npts,ss,nhsym,savg)    !Get normalized symbol spectra
-       call timer('symsp65 ',1)
-       !  endif
-       nfa=nf1
-       nfb=nf2
-       if(naggressive.gt.0 .and. ntol.lt.1000) then
-          nfa=max(200,nfqso-ntol)
-          nfb=min(4000,nfqso+ntol)
-          thresh0=1.0
-       endif
-
-       ! robust = .false.: use float ccf. Only if ncand>50 fall back to robust (1-bit) ccf
-       ! robust = .true. : use only robust (1-bit) ccf
-       ncand=0
-       if(.not.robust) then
-          call timer('sync65  ',0)
-          call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,0)
-          call timer('sync65  ',1)
-       endif
-       if(ncand.gt.50) robust=.true.
-       if(robust) then
-          ncand=0
-          call timer('sync65  ',0)
-          call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,1)
-          call timer('sync65  ',1)
-       endif
-
-       call fqso_first(nfqso,ntol,ca,ncand)
-
-       nvec=ntrials
-       if(ncand.gt.75) then
-          !      write(*,*) 'Pass ',ipass,' ncandidates too large ',ncand
-          nvec=100
-       endif
-
-       df=12000.0/NFFT                     !df = 12000.0/8192 = 1.465 Hz
-       mode65=2**nsubmode
-       nflip=1                             !### temporary ###
-       nqd=0
-       decoded0=""
-       freq0=0.
-
-       do icand=1,ncand
-          freq=ca(icand)%freq
-          dtx=ca(icand)%dt
-          sync1=ca(icand)%sync
-          if(ipass.eq.1) ntry65a=ntry65a + 1
-          if(ipass.eq.2) ntry65b=ntry65b + 1
-          call timer('decod65a',0)
-          call decode65a(dd,npts,first_time,nqd,freq,nflip,mode65,nvec,     &
-               naggressive,ndepth,mycall,hiscall,hisgrid,nexp_decode,   &
-               sync2,a,dtx,nft,qual,nhist,decoded)
-          call timer('decod65a',1)
-          n=naggressive
-          rtt=0.001*nrtt1000
-          if(nft.lt.2) then
-             if(nhard_min.gt.50) cycle
-             if(nhard_min.gt.h0(n)) cycle
-             if(ntotal_min.gt.d0(n)) cycle
-             if(rtt.gt.r0(n)) cycle
-          endif
-
-!          !### Suppress false decodes in crowded HF bands ###
-!          if(naggressive.eq.0 .and. ntrials.le.10000) then
-!             if(ntry.eq.ntrials) then
-!                if(nhard_min.ge.42 .or. ntotal_min.ge.71) cycle
-!             endif
-!          endif
-          if(decoded.eq.decoded0 .and. abs(freq-freq0).lt. 3.0 .and.    &
-               minsync.ge.0) cycle                  !Don't display dupes
-          if(decoded.ne.'                      ' .or. minsync.lt.0) then
-             if( nsubtract .eq. 1 ) then
-                call timer('subtr65 ',0)
-                call subtract65(dd,npts,freq,dtx)
-                call timer('subtr65 ',1)
-             endif
-             nfreq=nint(freq+a(1))
-             ndrift=nint(2.0*a(2))
-             s2db=10.0*log10(sync2) - 35             !### empirical ###
-             nsnr=nint(s2db)
-             if(nsnr.lt.-30) nsnr=-30
-             if(nsnr.gt.-1) nsnr=-1
-
-             ndupe=0 ! de-dedupe
-             do i=1, ndecoded
-                if(decoded==dec(i)%decoded) then
-                   ndupe=1
-                   exit
-                endif
-             enddo
-             if(ndupe.ne.1 .or. minsync.lt.0) then
-                if(ipass.eq.1) n65a=n65a + 1
-                if(ipass.eq.2) n65b=n65b + 1
-                ndecoded=ndecoded+1
-                dec(ndecoded)%freq=freq+a(1)
-                dec(ndecoded)%dt=dtx
-                dec(ndecoded)%sync=sync2
-                dec(ndecoded)%decoded=decoded
-                nqual=min(qual,9999.0)
-                !          if(nqual.gt.10) nqual=10
-                if (associated(this%callback)) then
-                   call this%callback(nutc,sync1,nsnr,dtx-1.0,nfreq,ndrift,decoded &
-                        ,nft,nqual,ncandidates,ntry,ntotal_min,nhard_min,naggressive)
-                end if
-             endif
-             decoded0=decoded
-             freq0=freq
-             if(decoded0.eq.'                      ') decoded0='*'
-          endif
-       enddo                                 !candidate loop
-       if(ndecoded.lt.1) exit
-    enddo                                   !two-pass loop
-
-    return
-  end subroutine decode
-
-end module jt65_decode
+module jt65_decode
+
+  integer, parameter :: NSZ=3413, NZMAX=60*12000, NFFT=1000
+
+  type :: jt65_decoder
+     procedure(jt65_decode_callback), pointer :: callback => null()
+   contains
+     procedure :: decode
+  end type jt65_decoder
+
+  !
+  ! Callback function to be called with each decode
+  !
+  abstract interface
+     subroutine jt65_decode_callback (this, utc, sync, snr, dt, freq, drift,          &
+          decoded, ft, qual, candidates, tries, total_min, hard_min, aggression)
+       import jt65_decoder
+       implicit none
+       class(jt65_decoder), intent(inout) :: this
+       integer, intent(in) :: utc
+       real, intent(in) :: sync
+       integer, intent(in) :: snr
+       real, intent(in) :: dt
+       integer, intent(in) :: freq
+       integer, intent(in) :: drift
+       character(len=22), intent(in) :: decoded
+       integer, intent(in) :: ft
+       integer, intent(in) :: qual
+       integer, intent(in) :: candidates
+       integer, intent(in) :: tries
+       integer, intent(in) :: total_min
+       integer, intent(in) :: hard_min
+       integer, intent(in) :: aggression
+     end subroutine jt65_decode_callback
+  end interface
+
+contains
+
+  subroutine decode(this,callback,dd0,npts,newdat,nutc,nf1,nf2,nfqso,ntol,nsubmode,   &
+       minsync,nagain,n2pass,nrobust,ntrials,naggressive,ndepth,       &
+       mycall,hiscall,hisgrid,nexp_decode)
+
+    !  Process dd0() data to find and decode JT65 signals.
+
+    use timer_module, only: timer
+
+    include 'constants.f90'
+
+    class(jt65_decoder), intent(inout) :: this
+    procedure(jt65_decode_callback) :: callback
+    real, intent(in) :: dd0(NZMAX)
+    integer, intent(in) :: npts, nutc, nf1, nf2, nfqso, ntol     &
+         , nsubmode, minsync, n2pass, ntrials, naggressive, ndepth      &
+         , nexp_decode
+    logical, intent(in) :: newdat, nagain, nrobust
+    character(len=12), intent(in) :: mycall, hiscall
+    character(len=6), intent(in) :: hisgrid
+
+    real dd(NZMAX)
+    real ss(322,NSZ)
+    real savg(NSZ)
+    real a(5)
+    character*22 decoded,decoded0
+    type candidate
+       real freq
+       real dt
+       real sync
+    end type candidate
+    type(candidate) ca(300)
+    type accepted_decode
+       real freq
+       real dt
+       real sync
+       character*22 decoded
+    end type accepted_decode
+    type(accepted_decode) dec(50)
+    logical :: first_time, robust
+
+    integer h0(0:11),d0(0:11),ne(0:11)
+    real r0(0:11)
+    common/decstats/ntry65a,ntry65b,n65a,n65b,num9,numfano
+    common/steve/thresh0
+    common/test000/ncandidates,nhard_min,nsoft_min,nera_best,nrtt1000,   &
+         ntotal_min,ntry,nq1000,npp1         !### TEST ONLY ###
+
+!            0  1  2  3  4  5  6  7  8  9 10 11
+    data h0/41,42,43,43,44,45,46,47,48,48,49,49/
+    data d0/71,72,73,74,76,77,78,80,81,82,83,83/
+
+!             0    1    2    3    4    5    6    7    8    9   10   11
+    data r0/0.70,0.72,0.74,0.76,0.78,0.80,0.82,0.84,0.86,0.88,0.90,0.90/
+    save
+
+    this%callback => callback
+    first_time=newdat
+    robust=nrobust
+    dd=dd0
+    ndecoded=0
+    do ipass=1,n2pass                             ! 2-pass decoding loop
+       first_time=.true.
+       if(ipass.eq.1) then                         !first-pass parameters
+          thresh0=2.5
+          nsubtract=1
+       elseif( ipass.eq.2 ) then !second-pass parameters
+          thresh0=2.5
+          nsubtract=0
+       endif
+       if(n2pass.lt.2) nsubtract=0
+
+       !  if(newdat) then
+       call timer('symsp65 ',0)
+       ss=0.
+       call symspec65(dd,npts,ss,nhsym,savg)    !Get normalized symbol spectra
+       call timer('symsp65 ',1)
+       !  endif
+       nfa=nf1
+       nfb=nf2
+       if(naggressive.gt.0 .and. ntol.lt.1000) then
+          nfa=max(200,nfqso-ntol)
+          nfb=min(4000,nfqso+ntol)
+          thresh0=1.0
+       endif
+
+       ! robust = .false.: use float ccf. Only if ncand>50 fall back to robust (1-bit) ccf
+       ! robust = .true. : use only robust (1-bit) ccf
+       ncand=0
+       if(.not.robust) then
+          call timer('sync65  ',0)
+          call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,0)
+          call timer('sync65  ',1)
+       endif
+       if(ncand.gt.50) robust=.true.
+       if(robust) then
+          ncand=0
+          call timer('sync65  ',0)
+          call sync65(ss,nfa,nfb,naggressive,ntol,nhsym,ca,ncand,1)
+          call timer('sync65  ',1)
+       endif
+
+       call fqso_first(nfqso,ntol,ca,ncand)
+
+       nvec=ntrials
+       if(ncand.gt.75) then
+          !      write(*,*) 'Pass ',ipass,' ncandidates too large ',ncand
+          nvec=100
+       endif
+
+       df=12000.0/NFFT                     !df = 12000.0/8192 = 1.465 Hz
+       mode65=2**nsubmode
+       nflip=1                             !### temporary ###
+       nqd=0
+       decoded0=""
+       freq0=0.
+
+       do icand=1,ncand
+          freq=ca(icand)%freq
+          dtx=ca(icand)%dt
+          sync1=ca(icand)%sync
+          if(ipass.eq.1) ntry65a=ntry65a + 1
+          if(ipass.eq.2) ntry65b=ntry65b + 1
+          call timer('decod65a',0)
+          call decode65a(dd,npts,first_time,nqd,freq,nflip,mode65,nvec,     &
+               naggressive,ndepth,mycall,hiscall,hisgrid,nexp_decode,   &
+               sync2,a,dtx,nft,qual,nhist,decoded)
+          call timer('decod65a',1)
+          n=naggressive
+          rtt=0.001*nrtt1000
+          if(nft.lt.2) then
+             if(nhard_min.gt.50) cycle
+             if(nhard_min.gt.h0(n)) cycle
+             if(ntotal_min.gt.d0(n)) cycle
+             if(rtt.gt.r0(n)) cycle
+          endif
+
+!          !### Suppress false decodes in crowded HF bands ###
+!          if(naggressive.eq.0 .and. ntrials.le.10000) then
+!             if(ntry.eq.ntrials) then
+!                if(nhard_min.ge.42 .or. ntotal_min.ge.71) cycle
+!             endif
+!          endif
+          if(decoded.eq.decoded0 .and. abs(freq-freq0).lt. 3.0 .and.    &
+               minsync.ge.0) cycle                  !Don't display dupes
+          if(decoded.ne.'                      ' .or. minsync.lt.0) then
+             if( nsubtract .eq. 1 ) then
+                call timer('subtr65 ',0)
+                call subtract65(dd,npts,freq,dtx)
+                call timer('subtr65 ',1)
+             endif
+             nfreq=nint(freq+a(1))
+             ndrift=nint(2.0*a(2))
+             s2db=10.0*log10(sync2) - 35             !### empirical ###
+             nsnr=nint(s2db)
+             if(nsnr.lt.-30) nsnr=-30
+             if(nsnr.gt.-1) nsnr=-1
+
+             ndupe=0 ! de-dedupe
+             do i=1, ndecoded
+                if(decoded==dec(i)%decoded) then
+                   ndupe=1
+                   exit
+                endif
+             enddo
+             if(ndupe.ne.1 .or. minsync.lt.0) then
+                if(ipass.eq.1) n65a=n65a + 1
+                if(ipass.eq.2) n65b=n65b + 1
+                ndecoded=ndecoded+1
+                dec(ndecoded)%freq=freq+a(1)
+                dec(ndecoded)%dt=dtx
+                dec(ndecoded)%sync=sync2
+                dec(ndecoded)%decoded=decoded
+                nqual=min(qual,9999.0)
+                !          if(nqual.gt.10) nqual=10
+                if (associated(this%callback)) then
+                   call this%callback(nutc,sync1,nsnr,dtx-1.0,nfreq,ndrift,decoded &
+                        ,nft,nqual,ncandidates,ntry,ntotal_min,nhard_min,naggressive)
+                end if
+             endif
+             decoded0=decoded
+             freq0=freq
+             if(decoded0.eq.'                      ') decoded0='*'
+          endif
+       enddo                                 !candidate loop
+       if(ndecoded.lt.1) exit
+    enddo                                   !two-pass loop
+
+    return
+  end subroutine decode
+
+end module jt65_decode
diff --git a/mainwindow.cpp b/mainwindow.cpp
index a4e0619c8..870c89a4b 100644
--- a/mainwindow.cpp
+++ b/mainwindow.cpp
@@ -35,7 +35,6 @@
 #include "messageaveraging.h"
 #include "widegraph.h"
 #include "sleep.h"
-#include "getfile.h"
 #include "logqso.h"
 #include "Radio.hpp"
 #include "Bands.hpp"
@@ -48,6 +47,7 @@
 #include "signalmeter.h"
 #include "HelpTextWindow.hpp"
 #include "SampleDownloader.hpp"
+#include "Audio/WavFile.hpp"
 
 #include "ui_mainwindow.h"
 #include "moc_mainwindow.cpp"
@@ -98,6 +98,7 @@ extern "C" {
 
   void fast_decode_(short id2[], int narg[], char msg[], int len);
   void degrade_snr_(short d2[], int* n, float* db);
+  void wav12_(short d2[], short d1[], int* nbytes, short* nbitsam2);
 }
 
 int volatile itone[NUM_ISCAT_SYMBOLS];	//Audio tones for all Tx symbols
@@ -614,13 +615,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
   m_wideGraph->setMode(m_mode);
   m_wideGraph->setModeTx(m_modeTx);
 
-  future1 = new QFuture<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()
 {
diff --git a/mainwindow.h b/mainwindow.h
index 97bef15cf..838a97701 100644
--- a/mainwindow.h
+++ b/mainwindow.h
@@ -93,7 +93,6 @@ public slots:
   void dataSink(qint64 frames);
   void fastSink(qint64 frames);
   void diskDat();
-  void diskWriteFinished();
   void freezeDecode(int n);
   void guiUpdate();
   void doubleClickOnCall(bool shift, bool ctrl);
@@ -436,12 +435,10 @@ private:
 
   QMessageBox msgBox0;
 
-  QFuture<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