code:
#include "APRSISClient.h"

#include <cmath>

#include "DriftingDateTime.h"
#include "varicode.h"

const int PACKET_TIMEOUT_SECONDS = 300;

APRSISClient::APRSISClient(QString host, quint16 port, QObject *parent):
    QTcpSocket(parent)
{
    setServer(host, port);

    connect(&m_timer, &QTimer::timeout, this, &APRSISClient::sendReports);
    m_timer.setInterval(60*1000); // every 60 seconds
    m_timer.start();
}

quint32 APRSISClient::hashCallsign(QString callsign){
    // based on: https://github.com/hessu/aprsc/blob/master/src/passcode.c
    QByteArray rootCall = QString(callsign.split("-").first().toUpper()).toLocal8Bit() + '\0';
    quint32 hash = 0x73E2;

    int i = 0;
    int len = rootCall.length();

    while(i+1 < len){
        hash ^= rootCall.at(i) << 8;
        hash ^= rootCall.at(i+1);
        i += 2;
    }

    return hash & 0x7FFF;
}

QString APRSISClient::loginFrame(QString callsign){
    auto loginFrame = QString("user %1 pass %2 ver %3\n");
    loginFrame = loginFrame.arg(callsign);
    loginFrame = loginFrame.arg(hashCallsign(callsign));
    loginFrame = loginFrame.arg("JS8Call");
    return loginFrame;
}

QList<QStringList> findall(QRegularExpression re, QString content){
    int pos = 0;

    QList<QStringList> all;
    while(pos < content.length()){
        auto match = re.match(content, pos);
        if(!match.hasMatch()){
            break;
        }

        all.append(match.capturedTexts());
        pos = match.capturedEnd();
    }

    return all;
}

inline long
floordiv (long num, long den)
{
  if (0 < (num^den))
    return num/den;
  else
    {
      ldiv_t res = ldiv(num,den);
      return (res.rem)? res.quot-1
                      : res.quot;
    }
}

// convert an arbitrary length grid locator to a high precision lat/lon
QPair<float, float> APRSISClient::grid2deg(QString locator){
    QString grid = locator.toUpper();

    float lat = -90;
    float lon = -90;

    auto lats = findall(QRegularExpression("([A-X])([A-X])"), grid);
    auto lons = findall(QRegularExpression("(\\d)(\\d)"), grid);

    int valx[22];
    int valy[22];

    int i = 0;
    int tot = 0;
    char A = 'A';
    foreach(QStringList matched, lats){
        char x = matched.at(1).at(0).toLatin1();
        char y = matched.at(2).at(0).toLatin1();

        valx[i*2] = x-A;
        valy[i*2] = y-A;

        i++;
        tot++;
    }

    i = 0;
    foreach(QStringList matched, lons){
        int x = matched.at(1).toInt();
        int y = matched.at(2).toInt();
        valx[i*2+1]=x;
        valy[i*2+1]=y;

        i++;
        tot++;
    }

    for(int i = 0; i < tot; i++){
        int x = valx[i];
        int y = valy[i];

        int z = i - 1;
        float scale = pow(10, floordiv(-(z-1), 2)) * pow(24, floordiv(-z, 2));

        lon += scale * x;
        lat += scale * y;
    }

    lon *= 2;

    return {lat, lon};
}

// convert an arbitrary length grid locator to a high precision lat/lon in aprs format
QPair<QString, QString> APRSISClient::grid2aprs(QString grid){
    auto geo = APRSISClient::grid2deg(grid);
    auto lat = geo.first;
    auto lon = geo.second;

    QString latDir = "N";
    if(lat < 0){
        lat *= -1;
        latDir = "S";
    }

    QString lonDir = "E";
    if(lon < 0){
        lon *= -1;
        lonDir = "W";
    }

    double iLat, fLat, iLon, fLon, iLatMin, fLatMin, iLonMin, fLonMin, iLatSec, iLonSec;
    fLat = modf(lat, &iLat);
    fLon = modf(lon, &iLon);

    fLatMin = modf(fLat * 60, &iLatMin);
    fLonMin = modf(fLon * 60, &iLonMin);

    iLatSec = round(fLatMin * 60);
    iLonSec = round(fLonMin * 60);

    if(iLatSec == 60){
        iLatMin += 1;
        iLatSec = 0;
    }

    if(iLonSec == 60){
        iLonMin += 1;
        iLonSec = 0;
    }

    if(iLatMin == 60){
        iLat += 1;
        iLatMin = 0;
    }

    if(iLonMin == 60){
        iLon += 1;
        iLonMin = 0;
    }

    double aprsLat = iLat * 100 + iLatMin + (iLatSec / 60.0);
    double aprsLon = iLon * 100 + iLonMin + (iLonSec / 60.0);

    return {
        QString().sprintf("%07.2f%%1", aprsLat).arg(latDir),
        QString().sprintf("%08.2f%%1", aprsLon).arg(lonDir)
    };
}

QString APRSISClient::stripSSID(QString call){
    return QString(call.split("-").first().toUpper());
}

QString APRSISClient::replaceCallsignSuffixWithSSID(QString call, QString base){
    if(call != base){
        QRegularExpression re("[/](?<ssid>(P|\\d+))");
        auto matcher = re.globalMatch(call);
        if(matcher.hasNext()){
            auto match = matcher.next();
            auto ssid = match.captured("ssid");
            if(ssid == "P"){
                ssid = "16";
            }
            call = base + "-" + ssid;
        } else {
            call = base;
        }
    }
    return call;
}

void APRSISClient::enqueueSpot(QString theircall, QString grid, QString comment){
    if(m_localCall.isEmpty()) return;

    auto geo = APRSISClient::grid2aprs(grid);
    auto spotFrame = QString("%1>APRS,%2,TCPIP*:=%3/%4nJS8 %5\n");
    spotFrame = spotFrame.arg(theircall);
    spotFrame = spotFrame.arg(stripSSID(m_localCall));
    spotFrame = spotFrame.arg(geo.first);
    spotFrame = spotFrame.arg(geo.second);
    spotFrame = spotFrame.arg(comment.left(43));
    enqueueRaw(spotFrame);
}

void APRSISClient::enqueueThirdParty(QString theircall, QString payload){
    if(!isPasscodeValid()){
        return;
    }

    auto frame = QString("%1>APRS,%2,TCPIP*:%3\n");
    frame = frame.arg(theircall);
    frame = frame.arg(stripSSID(m_localCall));
    frame = frame.arg(payload);
    enqueueRaw(frame);
}

void APRSISClient::enqueueRaw(QString aprsFrame){
    m_frameQueue.enqueue({ aprsFrame, DriftingDateTime::currentDateTimeUtc() });
}

void APRSISClient::processQueue(bool disconnect){
    // don't process queue if we haven't set our local callsign
    if(m_localCall.isEmpty()) return;

    // don't process queue if there's nothing to process
    if(m_frameQueue.isEmpty()) return;

    // 1. connect (and read)
    // 2. login (and read)
    // 3. for each raw frame in queue, send
    // 4. disconnect

    if(state() != QTcpSocket::ConnectedState){
        qDebug() << "APRSISClient Connecting:" << m_host << m_port;
        connectToHost(m_host, m_port);
        if(!waitForConnected(5000)){
            qDebug() << "APRSISClient Connection Error:" << errorString();
            return;
        }
    }

    auto re = QRegExp("(full|unavailable|busy)");
    auto line = QString(readLine());
    if(line.toLower().indexOf(re) >= 0){
        qDebug() << "APRSISClient Connection Busy:" << line;
        return;
    }

    if(write(loginFrame(m_localCall).toLocal8Bit()) == -1){
        qDebug() << "APRSISClient Write Login Error:" << errorString();
        return;
    }

    if(!waitForReadyRead(5000)){
        qDebug() << "APRSISClient Login Error: Server Not Responding";
        return;
    }

    line = QString(readAll());
    if(line.toLower().indexOf(re) >= 0){
        qDebug() << "APRSISClient Server Busy:" << line;
        return;
    }

    QQueue<QPair<QString, QDateTime>> delayed;

    while(!m_frameQueue.isEmpty()){
        auto pair = m_frameQueue.head();
        auto frame = pair.first;
        auto timestamp = pair.second;

        // if the packet is older than the timeout, drop it.
        if(timestamp.secsTo(DriftingDateTime::currentDateTimeUtc()) > PACKET_TIMEOUT_SECONDS){
            qDebug() << "APRSISClient Packet Timeout:" << frame;
            m_frameQueue.dequeue();
            continue;
        }

        // random delay 25% of the time for throttling (a skip will add 60 seconds to the processing time)
        if(qrand() % 100 <= 25){
            qDebug() << "APRSISClient Throttle: Skipping Frame";
            delayed.enqueue(m_frameQueue.dequeue());
            continue;
        }

        QByteArray data = frame.toLocal8Bit();
        if(write(data) == -1){
            qDebug() << "APRSISClient Write Error:" << errorString();
            return;
        }

        qDebug() << "APRSISClient Write:" << data;
        if(waitForReadyRead(5000)){
            line = QString(readLine());

            qDebug() << "APRSISClient Read:" << line;

            if(line.toLower().indexOf(re) >= 0){
                qDebug() << "APRSISClient Cannot Write Error:" << line;
                return;
            }
        }

        m_frameQueue.dequeue();
    }

    // enqueue the delayed frames for later processing
    while(!delayed.isEmpty()){
        m_frameQueue.enqueue(delayed.dequeue());
    }

    if(disconnect){
        disconnectFromHost();
    }
}


code:
#ifndef APRSISCLIENT_H
#define APRSISCLIENT_H

#include <QtGlobal>
#include <QDateTime>
#include <QTcpSocket>
#include <QQueue>
#include <QPair>
#include <QTimer>

class APRSISClient : public QTcpSocket
{
public:
    APRSISClient(QString host, quint16 port, QObject *parent = nullptr);

    static quint32 hashCallsign(QString callsign);
    static QString loginFrame(QString callsign);
    static QPair<float, float> grid2deg(QString grid);
    static QPair<QString, QString> grid2aprs(QString grid);
    static QString stripSSID(QString call);
    static QString replaceCallsignSuffixWithSSID(QString call, QString base);

    void setServer(QString host, quint16 port){
        if(state() == QTcpSocket::ConnectedState){
            disconnectFromHost();
        }

        m_host = host;
        m_port = port;

        qDebug() << "APRSISClient Server Change:" << m_host << m_port;
    }

    void setPaused(bool paused){
        m_paused = paused;
    }

    void setLocalStation(QString mycall, QString mygrid, QString passcode){
        m_localCall = mycall;
        m_localGrid = mygrid;
        m_localPasscode = passcode;
    }

    bool isPasscodeValid(){ return m_localPasscode == QString::number(hashCallsign(m_localCall)); }

    void enqueueSpot(QString theircall, QString grid, QString comment);
    void enqueueThirdParty(QString theircall, QString payload);
    void enqueueRaw(QString aprsFrame);

    void processQueue(bool disconnect=true);

public slots:
    void sendReports(){
        if(m_paused) return;

        processQueue(true);
    }

private:
    QString m_localCall;
    QString m_localGrid;
    QString m_localPasscode;

    QQueue<QPair<QString, QDateTime>> m_frameQueue;
    QString m_host;
    quint16 m_port;
    QTimer m_timer;
    bool m_paused;
};

#endif // APRSISCLIENT_H

license: mit


code:
#include "BWFFile.hpp"

#include <cstring>
#include <numeric>
#include <algorithm>

#include <qendian.h>
#include <QAudioFormat>
#include <QDateTime>
#include <QDate>
#include <QTime>
#include <QString>
#include <QUuid>

#include "pimpl_impl.hpp"

#include "moc_BWFFile.cpp"

namespace
{
  // chunk descriptor
  struct Desc
  {
    Desc ()
      : size_ {0}
    {
    }

    explicit Desc (char const * id, quint32 size = 0)
      : size_ {size}
    {
      set (id);
    }

    void set (char const * id = nullptr)
    {
      if (id)
        {
          auto len = std::min (size_t (4), strlen (id));
          memcpy (id_.data (), id, len);
          if (len < 4u)
            {
              memset (id_.data () + len, ' ', 4u - len);
            }
        }
      else
        {
          memcpy (id_.data (), "JUNK", 4);
        }
    }

    void set (char const * id, quint32 size)
    {
      set (id);
      size_ = size;
    }

    char * operator & () {return reinterpret_cast<char *> (this);}
    char const * operator & () const {return reinterpret_cast<char const *> (this);}

    std::array<char, 4> id_;
    quint32 size_;
  };

  // "fmt " chunk contents
  struct FormatChunk
  {
    quint16 audio_format;
    quint16 num_channels;
    quint32 sample_rate;
    quint32 byte_rate;
    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_[];
  };
}

class BWFFile::impl final
{
public:
  using FileError = QFile::FileError;

  impl (QAudioFormat const& format)
    : header_dirty_ {true}
    , format_ {format}
    , header_length_ {-1}
    , data_size_ {-1}
    , error_ {FileError::NoError}
  {
  }

  impl (QAudioFormat const& format, QString const& name)
    : header_dirty_ {true}
    , format_ {format}
    , file_ {name}
    , header_length_ {-1}
    , data_size_ {-1}
    , error_ {FileError::NoError}
  {
  }

  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}
    , error_ {FileError::NoError}
  {
  }

  ~impl ()
  {
    if (file_.isOpen ()) file_.close ();
  }

  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_;
  FileError error_;
};

bool BWFFile::impl::initialize (BWFFile * self, OpenMode mode)
{
  bool result {false};
  if (mode & Append)
    {
      result = file_.seek (file_.size ());
      if (result) result = self->seek (file_.size () - header_length_);
    }
  else
    {
      result = self->seek (0);
    }
  return result;
}

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 ();
  quint32 outer_size {0};
  bool be {false};
  while (outer_offset < sizeof outer_desc + outer_desc.size_ - 1) // allow for uncounted pad
    {
      if (file_.read (&outer_desc, sizeof outer_desc) != sizeof outer_desc) return false;
      be = !memcmp (&outer_desc.id_, "RIFX", 4);
      outer_size = be ? qFromBigEndian<quint32> (outer_desc.size_) : qFromLittleEndian<quint32> (outer_desc.size_);
      if (!memcmp (&outer_desc.id_, "RIFF", 4) || be)
        {
          // RIFF or RIFX
          char riff_item[4];
          if (file_.read (riff_item, sizeof riff_item) != sizeof riff_item) return false;
          if (!memcmp (riff_item, "WAVE", 4))
            {
              // WAVE
              Desc wave_desc;
              quint32 wave_offset = file_.pos ();
              quint32 wave_size {0};
              while (wave_offset < outer_offset + sizeof outer_desc + outer_size - 1)
                {
                  if (file_.read (&wave_desc, sizeof wave_desc) != sizeof wave_desc) return false;
                  wave_size = be ? qFromBigEndian<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;
                      if (file_.read (reinterpret_cast<char *> (&fmt), sizeof fmt) != sizeof fmt) return false;
                      auto audio_format = be ? qFromBigEndian<quint16> (fmt.audio_format) : qFromLittleEndian<quint16> (fmt.audio_format);
                      if (audio_format != 0 && audio_format != 1) return false; // not PCM nor undefined
                      format_.setByteOrder (be ? QAudioFormat::BigEndian : QAudioFormat::LittleEndian);
                      format_.setChannelCount (be ? qFromBigEndian<quint16> (fmt.num_channels) : qFromLittleEndian<quint16> (fmt.num_channels));
                      format_.setCodec ("audio/pcm");
                      format_.setSampleRate (be ? qFromBigEndian<quint32> (fmt.sample_rate) : qFromLittleEndian<quint32> (fmt.sample_rate));
                      int bits_per_sample {be ? qFromBigEndian<quint16> (fmt.bits_per_sample) : qFromLittleEndian<quint16> (fmt.bits_per_sample)};
                      format_.setSampleSize (bits_per_sample);
                      format_.setSampleType (8 == bits_per_sample ? QAudioFormat::UnSignedInt : QAudioFormat::SignedInt);
                    }
                  else if (!memcmp (&wave_desc.id_, "data", 4))
                    {
                      data_size_ = wave_size;
                      header_length_ = file_.pos ();
                    }
                  else if (!memcmp (&wave_desc.id_, "LIST", 4))
                    {
                      char list_type[4];
                      if (file_.read (list_type, sizeof list_type) != sizeof list_type) return false;
                      if (!memcmp (list_type, "INFO", 4))
                        {
                          Desc info_desc;
                          quint32 info_offset = file_.pos ();
                          quint32 info_size {0};
                          while (info_offset < wave_offset + sizeof wave_desc + wave_size - 1)
                            {
                              if (file_.read (&info_desc, sizeof info_desc) != sizeof info_desc) return false;
                              info_size = be ? qFromBigEndian<quint32> (info_desc.size_) : qFromLittleEndian<quint32> (info_desc.size_);
                              info_dictionary_[info_desc.id_] = file_.read (info_size);
                              if (!file_.seek (info_offset + sizeof info_desc + (info_size + 1) / 2 * 2)) return false;;
                              info_offset = file_.pos ();
                            }
                        }
                    }
                  if (!file_.seek (wave_offset + sizeof wave_desc + (wave_size + 1) / 2 * 2)) return false;
                  wave_offset = file_.pos ();
                }
            }
        }
      if (!file_.seek (outer_offset + sizeof outer_desc + (outer_size + 1) / 2 * 2)) return false;
      outer_offset = file_.pos ();
    }
  return data_size_ >= 0 && file_.seek (header_length_);
}

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 ()};
  Desc desc {be ? "RIFX" : "RIFF"};
  if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
  header_dirty_ = true;
  if (file_.write ("WAVE", 4) != 4) return false;

  FormatChunk fmt;
  if (be)
    {
      fmt.audio_format = qToBigEndian<quint16> (1); // PCM
      fmt.num_channels = qToBigEndian<quint16> (format.channelCount ());
      fmt.sample_rate = qToBigEndian<quint32> (format.sampleRate ());
      fmt.byte_rate = qToBigEndian<quint32> (format.bytesForDuration (1000000));
      fmt.block_align = qToBigEndian<quint16> (format.bytesPerFrame ());
      fmt.bits_per_sample = qToBigEndian<quint16> (format.sampleSize ());
      desc.set ("fmt", qToBigEndian<quint32> (sizeof fmt));
    }
  else
    {
      fmt.audio_format = qToLittleEndian<quint16> (1); // PCM
      fmt.num_channels = qToLittleEndian<quint16> (format.channelCount ());
      fmt.sample_rate = qToLittleEndian<quint32> (format.sampleRate ());
      fmt.byte_rate = qToLittleEndian<quint32> (format.bytesForDuration (1000000));
      fmt.block_align = qToLittleEndian<quint16> (format.bytesPerFrame ());
      fmt.bits_per_sample = qToLittleEndian<quint16> (format.sampleSize ());
      desc.set ("fmt", qToLittleEndian<quint32> (sizeof fmt));
    }
  if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
  if (file_.write (reinterpret_cast<char const *> (&fmt), sizeof fmt) != sizeof fmt) return false;

  desc.set ("data");
  if (file_.write (&desc, sizeof desc) != sizeof desc) return false;
  header_length_ = file_.pos ();
  return true;
}

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;
  desc.set ("data", be ? qToBigEndian<quint32> (size) : qToLittleEndian<quint32> (size));
  if (file_.write (&desc, sizeof desc) != sizeof desc) return false;

  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;
  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);
}

//
// BWFFile implementation
//
BWFFile::BWFFile (QAudioFormat const& format, QObject * parent)
  : QIODevice {parent}
  , m_ {format}
{
}

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 success {false};
  if (!(mode & WriteOnly))
    {
      if (!(success = m_->file_.open (mode & ~Text)))
        {
          setErrorString (m_->file_.errorString ());
        }
      else
        {
          if (!(success = m_->read_header ()))
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to read file header");
                  m_->error_ = FileError::ReadError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
            }
        }
    }
  else
    {
      if (!(success = m_->file_.open (mode & ~Text)))
        {
          setErrorString (m_->file_.errorString ());
        }
      else
        {
          if (!(success = (m_->read_header () || m_->write_header (m_->format_))))
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to update file header");
                  m_->error_ = FileError::WriteError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
            }
          else if (!(success = m_->file_.resize (m_->header_length_)))
            {
              setErrorString (m_->file_.errorString ());
            }
        }
    }
  success && (success = QIODevice::open (mode | Unbuffered));
  if (success && !(success = m_->initialize (this, mode)))
    {
      if (FileError::NoError == m_->file_.error ())
        {
          setErrorString ("Unable to initialize file header");
          m_->error_ = FileError::WriteError;
        }
      else
        {
          setErrorString (m_->file_.errorString ());
        }
    }
  if (!success) close ();
  return success;
}

bool BWFFile::open(FILE * fh, OpenMode mode, FileHandleFlags flags)
{
  bool success {false};
  if (!(mode & ReadOnly))
    {
      setErrorString ("Read or read/write access must be specified");
      m_->error_ = FileError::OpenError;
      return success;
    }
  if (!(mode & WriteOnly))
    {
      if (!(success = m_->file_.open (fh, mode & ~Text, flags)))
        {
          setErrorString (m_->file_.errorString ());
        }
      else
        {
          if (!(success = m_->read_header ()))
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to read file header");
                  m_->error_ = FileError::ReadError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
            }
        }
    }
  else
    {
      if (!(success = m_->file_.open (fh, mode & ~Text, flags)))
        {
          setErrorString (m_->file_.errorString ());
        }
      else
        {
          if (!(success = (m_->read_header () || m_->write_header (m_->format_))))
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to update file header");
                  m_->error_ = FileError::WriteError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
            }
          else if (!(success = m_->file_.resize (m_->header_length_)))
            {
              setErrorString (m_->file_.errorString ());
            }
        }
    }
  success && (success = QIODevice::open (mode | Unbuffered));
  if (success && !(success = m_->initialize (this, mode)))
    {
      if (FileError::NoError == m_->file_.error ())
        {
          setErrorString ("Unable to initialize file header");
          m_->error_ = FileError::WriteError;
        }
      else
        {
          setErrorString (m_->file_.errorString ());
        }
    }
  if (!success) close ();
  return success;
}

bool BWFFile::open (int fd, OpenMode mode, FileHandleFlags flags)
{
  bool success {false};
  if (!(mode & ReadOnly))
    {
      setErrorString ("Read or read/write access must be specified");
      m_->error_ = FileError::OpenError;
      return success;
    }
  if (!(mode & WriteOnly))
    {
      if (!(success = m_->file_.open (fd, mode & ~Text, flags)))
        {
          setErrorString (m_->file_.errorString ());
        }
      else
        {
          if (!(success = m_->read_header ()))
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to read file header");
                  m_->error_ = FileError::ReadError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
            }
        }
    }
  else
    {
      if (!(success = m_->file_.open (fd, mode & ~Text, flags)))
        {
          setErrorString (m_->file_.errorString ());
        }
      else
        {
          if (!(success = (m_->read_header () || m_->write_header (m_->format_))))
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to update file header");
                  m_->error_ = FileError::WriteError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
            }
          else if (!(success = m_->file_.resize (m_->header_length_)))
            {
              setErrorString (m_->file_.errorString ());
            }
        }
    }
  success && (success = QIODevice::open (mode | Unbuffered));
  if (success && !(success = m_->initialize (this, mode)))
    {
      if (FileError::NoError == m_->file_.error ())
        {
          setErrorString ("Unable to initialize file header");
          m_->error_ = FileError::WriteError;
        }
      else
        {
          setErrorString (m_->file_.errorString ());
        }
    }
  if (!success) close ();
  return success;
}

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 ()
{
  bool success {true};
  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_ > qint64 (3 * sizeof (Desc) + 4 + sizeof (FormatChunk)))
        {
          // we need to move the data down
          auto old_pos = m_->header_length_;
          if (!(success = m_->write_header (m_->format_)))
            {
              auto new_pos = m_->header_length_;
              QByteArray buffer;
              while (size)
                {
                  success = m_->file_.seek (old_pos);
                  if (!success)
                    {
                      setErrorString (m_->file_.errorString ());
                    }
                  else
                    {
                      buffer = m_->file_.read (std::min (size, qint64 (32768)));
                      success = m_->file_.seek (new_pos);
                      if (!success)
                        {
                          setErrorString (m_->file_.errorString ());
                        }
                      else
                        {
                          qint64 bytes = m_->file_.write (buffer);
                          if (bytes < 0)
                            {
                              setErrorString (m_->file_.errorString ());
                              success = false;
                            }
                          else
                            {
                              new_pos += buffer.size ();
                              old_pos += buffer.size ();
                              size -= buffer.size ();
                            }
                        }
                    }
                }
            }
          else
            {
              if (FileError::NoError == m_->file_.error ())
                {
                  setErrorString ("Unable to update file header");
                  m_->error_ = FileError::WriteError;
                }
              else
                {
                  setErrorString (m_->file_.errorString ());
                }
              return success;
            }
        }
      if (success)
        {
          success = m_->file_.resize (m_->header_length_ + m_->data_size_);
          if (!success)
            {
              setErrorString (m_->file_.errorString ());
            }
          m_->header_dirty_ = true;
        }
    }
  else
    {
      setErrorString ("File not open");
      m_->error_ = FileError::WriteError;
      success = false;
    }
  return success && QIODevice::reset ();
}

qint64 BWFFile::size () const
{
  return m_->data_size_ < 0 ? m_->file_.size () - m_->header_length_ : m_->data_size_;
}

bool BWFFile::isSequential () const
{
  return m_->file_.isSequential ();
}

void BWFFile::close ()
{
  if (isOpen ()) QIODevice::close ();
  if (m_->header_dirty_ || m_->data_size_ < 0)
    {
      m_->update_header ();
    }
  if (m_->file_.isOpen ())
    {
      m_->file_.close ();
    }
}

bool BWFFile::seek (qint64 pos)
{
  if (pos < 0)
    {
      setErrorString ("Attempt to seek before beginning of data");
      m_->error_ = FileError::PositionError;
      return false;
    }
  bool success = QIODevice::seek (pos);
  if (success)
    {
      success = m_->file_.seek (pos + m_->header_length_);
      if (!success) setErrorString (m_->file_.errorString ());
    }
  return success;
}

qint64 BWFFile::readData (char * data, qint64 max_size)
{
  return m_->file_.read (data, max_size);
}

qint64 BWFFile::writeData (char const* data, qint64 max_size)
{
  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)
{
  bool success = m_->file_.setPermissions (permissions);
  if (!success) setErrorString (m_->file_.errorString ());
  return success;
}

auto BWFFile::error () const -> FileError
{
  return m_->error_ != FileError::NoError ? m_->error_ : m_->file_.error ();
}

bool BWFFile::flush ()
{
  bool success = m_->file_.flush ();
  if (!success) setErrorString (m_->file_.errorString ());
  return success;
}

int BWFFile::handle () const
{
  int h {m_->file_.handle ()};
  if (h < 0) const_cast<BWFFile *> (this)->setErrorString (m_->file_.errorString ());
  return h;
}

uchar * BWFFile::map (qint64 offset, qint64 size, MemoryMapFlags flags)
{
  uchar * address = m_->file_.map (offset + m_->header_length_, size, flags);
  if (!address) setErrorString (m_->file_.errorString ());
  return address;
}

bool BWFFile::unmap (uchar * address)
{
  bool success = m_->file_.unmap (address);
  if (!success) setErrorString (m_->file_.errorString ());
  return success;
}

void BWFFile::unsetError ()
{
  m_->error_ = FileError::NoError;
  m_->file_.unsetError ();
}


code:
#ifndef BWF_FILE_HPP__
#define BWF_FILE_HPP__

#include <array>

#include <QFile>
#include <QMap>
#include <QByteArray>

#include "pimpl_h.hpp"

class QObject;
class QString;
class QAudioFormat;

//
// 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
public:
  using FileHandleFlags = QFile::FileHandleFlags;
  using Permissions = QFile::Permissions;
  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

  // 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;

  // 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;

  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;

  // this can fail due to updating header issues, errors are ignored
  void close () override;

protected:
  qint64 readData (char * data, qint64 max_size) override;
  qint64 writeData (char const* data, qint64 max_size) override;

private:
  class impl;
  pimpl<impl> m_;
};

#endif


code:
#include "AudioDevice.hpp"

bool AudioDevice::initialize (OpenMode mode, Channel channel)
{
  m_channel = channel;

  // open and ensure we are unbuffered if possible
  return QIODevice::open (mode | QIODevice::Unbuffered);
}


code:
#ifndef AUDIODEVICE_HPP__
#define AUDIODEVICE_HPP__

#include <QIODevice>

class QDataStream;

//
// abstract base class for audio devices
//
class AudioDevice : public QIODevice
{
public:
  enum Channel {Mono, Left, Right, Both}; // these are mapped to combobox index so don't change

  static char const * toString (Channel c)
  {
    return Mono == c ? "Mono" : Left == c ? "Left" : Right == c ? "Right" : "Both";
  }

  static Channel fromString (QString const& str)
  {
    QString s (str.toCaseFolded ().trimmed ().toLatin1 ());
    return "both" == s ? Both : "right" == s ? Right : "left" == s ? Left : Mono;
  }

  bool initialize (OpenMode mode, Channel channel);

  bool isSequential () const override {return true;}

  size_t bytesPerFrame () const {return sizeof (qint16) * (Mono == m_channel ? 1 : 2);}

  Channel channel () const {return m_channel;}

protected:
  AudioDevice (QObject * parent = 0)
    : QIODevice (parent)
  {
  }

  void store (char const * source, size_t numFrames, qint16 * dest)
  {
    qint16 const * begin (reinterpret_cast<qint16 const *> (source));
    for ( qint16 const * i = begin; i != begin + numFrames * (bytesPerFrame () / sizeof (qint16)); i += bytesPerFrame () / sizeof (qint16))
      {
	switch (m_channel)
	  {
	  case Mono:
	    *dest++ = *i;
	    break;

	  case Right:
	    *dest++ = *(i + 1);
	    break;

	  case Both:		// should be able to happen but if it
				// does we'll take left
	    Q_ASSERT (Both == m_channel);
	  case Left:
	    *dest++ = *i;
	    break;
	  }
      }
  }

  qint16 * load (qint16 const sample, qint16 * dest)
  {
    switch (m_channel)
      {
      case Mono:
	*dest++ = sample;
	break;

      case Left:
	*dest++ = sample;
	*dest++ = 0;
	break;

      case Right:
	*dest++ = 0;
	*dest++ = sample;
	break;

      case Both:
	*dest++ = sample;
	*dest++ = sample;
	break;
      }
    return dest;
  }

private:
  Channel m_channel;
};

Q_DECLARE_METATYPE (AudioDevice::Channel);

#endif


code:
#ifndef BANDS_HPP__
#define BANDS_HPP__

#include <QAbstractTableModel>

#include "Radio.hpp"

//
// Class Bands
//
//  Encapsulates information  about amateur radio bands  as defined by
//  the  ADIF specification.  The model  is immutable.   The rows  are
//  stored in asscending order of frequency.
//
// Responsibilities
//
//  Provides  a  well  known  band  name mapped  to  lower  and  upper
//  frequency  limits.   Also  provides  a  convenience  operation  to
//  determine the  band name  for any given  frequency, the  result of
//  which may  be null  if the  given frequency  doesn't lie  within a
//  recognised band.
//
// Collaborations
//
//  Implements the QAbstractTableModel interface as an immutable table
//  where rows  are bands and  columns are band name,  lower frequency
//  limit and, upper ferquency limit respectively.
//
class Bands final
  : public QAbstractTableModel
{
public:
  using Frequency = Radio::Frequency;

  // an iterator that meets the requirements of the C++ for range statement
  class const_iterator
  {
  public:
    const_iterator (int row)
      : row_ {row}
    {
    }

    QString operator * ();
    bool operator != (const_iterator const&) const;
    const_iterator& operator ++ ();

  private:
    int row_;
  };

  explicit Bands (QObject * parent = nullptr);

  //
  // Model API
  //
  QString find (Frequency) const; // find band Frequency is in
  int find (QString const&) const; // find row of band (-1 if not valid)
  bool findFreq(QString const& band, Radio::Frequency *pFreqLower, Radio::Frequency *pFreqHigher) const;

  static QString const& oob ();

  // Iterators
  const_iterator begin () const;
  const_iterator end () const;

  // Custom role for sorting.
  static int constexpr SortRole = Qt::UserRole;

  // Implement the QAbstractTableModel interface
  int rowCount (QModelIndex const& parent = QModelIndex {}) const override;
  int columnCount (QModelIndex const& parent = QModelIndex {}) const override;
  Qt::ItemFlags flags (QModelIndex const& = QModelIndex {}) const override;
  QVariant headerData (int section, Qt::Orientation, int = Qt::DisplayRole) const override;

  // The value return for the Qt::DisplayRole role for the root of the
  // model (invalid index) is a special string representing out of
  // band.
  //
  // All columns return a number for the custom role SortRole, this
  // number defines a strict frequency order for the rows.
  QVariant data (QModelIndex const&, int role = Qt::DisplayRole) const override;
};

#endif


code:
#.rst:
# BundleUtilities
# ---------------
#
# Functions to help assemble a standalone bundle application.
#
# A collection of CMake utility functions useful for dealing with .app
# bundles on the Mac and bundle-like directories on any OS.
#
# The following functions are provided by this module:
#
# ::
#
#    fixup_bundle
#    copy_and_fixup_bundle
#    verify_app
#    get_bundle_main_executable
#    get_dotapp_dir
#    get_bundle_and_executable
#    get_bundle_all_executables
#    get_item_key
#    get_item_rpaths
#    clear_bundle_keys
#    set_bundle_key_values
#    get_bundle_keys
#    copy_resolved_item_into_bundle
#    copy_resolved_framework_into_bundle
#    fixup_bundle_item
#    verify_bundle_prerequisites
#    verify_bundle_symlinks
#
# Requires CMake 2.6 or greater because it uses function, break and
# PARENT_SCOPE.  Also depends on GetPrerequisites.cmake.
#
# ::
#
#   FIXUP_BUNDLE(<app> <libs> <dirs>)
#
# Fix up a bundle in-place and make it standalone, such that it can be
# drag-n-drop copied to another machine and run on that machine as long
# as all of the system libraries are compatible.
#
# If you pass plugins to fixup_bundle as the libs parameter, you should
# install them or copy them into the bundle before calling fixup_bundle.
# The "libs" parameter is a list of libraries that must be fixed up, but
# that cannot be determined by otool output analysis.  (i.e., plugins)
#
# Gather all the keys for all the executables and libraries in a bundle,
# and then, for each key, copy each prerequisite into the bundle.  Then
# fix each one up according to its own list of prerequisites.
#
# Then clear all the keys and call verify_app on the final bundle to
# ensure that it is truly standalone.
#
# ::
#
#   COPY_AND_FIXUP_BUNDLE(<src> <dst> <libs> <dirs>)
#
# Makes a copy of the bundle <src> at location <dst> and then fixes up
# the new copied bundle in-place at <dst>...
#
# ::
#
#   VERIFY_APP(<app>)
#
# Verifies that an application <app> appears valid based on running
# analysis tools on it.  Calls "message(FATAL_ERROR" if the application
# is not verified.
#
# ::
#
#   GET_BUNDLE_MAIN_EXECUTABLE(<bundle> <result_var>)
#
# The result will be the full path name of the bundle's main executable
# file or an "error:" prefixed string if it could not be determined.
#
# ::
#
#   GET_DOTAPP_DIR(<exe> <dotapp_dir_var>)
#
# Returns the nearest parent dir whose name ends with ".app" given the
# full path to an executable.  If there is no such parent dir, then
# simply return the dir containing the executable.
#
# The returned directory may or may not exist.
#
# ::
#
#   GET_BUNDLE_AND_EXECUTABLE(<app> <bundle_var> <executable_var> <valid_var>)
#
# Takes either a ".app" directory name or the name of an executable
# nested inside a ".app" directory and returns the path to the ".app"
# directory in <bundle_var> and the path to its main executable in
# <executable_var>
#
# ::
#
#   GET_BUNDLE_ALL_EXECUTABLES(<bundle> <exes_var>)
#
# Scans the given bundle recursively for all executable files and
# accumulates them into a variable.
#
# ::
#
#   GET_ITEM_KEY(<item> <key_var>)
#
# Given a file (item) name, generate a key that should be unique
# considering the set of libraries that need copying or fixing up to
# make a bundle standalone.  This is essentially the file name including
# extension with "." replaced by "_"
#
# This key is used as a prefix for CMake variables so that we can
# associate a set of variables with a given item based on its key.
#
# ::
#
#   CLEAR_BUNDLE_KEYS(<keys_var>)
#
# Loop over the list of keys, clearing all the variables associated with
# each key.  After the loop, clear the list of keys itself.
#
# Caller of get_bundle_keys should call clear_bundle_keys when done with
# list of keys.
#
# ::
#
#   SET_BUNDLE_KEY_VALUES(<keys_var> <context> <item> <exepath> <dirs>
#                         <copyflag> [<rpaths>])
#
# Add a key to the list (if necessary) for the given item.  If added,
# also set all the variables associated with that key.
#
# ::
#
#   GET_BUNDLE_KEYS(<app> <libs> <dirs> <keys_var>)
#
# Loop over all the executable and library files within the bundle (and
# given as extra <libs>) and accumulate a list of keys representing
# them.  Set values associated with each key such that we can loop over
# all of them and copy prerequisite libs into the bundle and then do
# appropriate install_name_tool fixups.
#
# ::
#
#   COPY_RESOLVED_ITEM_INTO_BUNDLE(<resolved_item> <resolved_embedded_item>)
#
# Copy a resolved item into the bundle if necessary.  Copy is not
# necessary if the resolved_item is "the same as" the
# resolved_embedded_item.
#
# ::
#
#   COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE(<resolved_item> <resolved_embedded_item>)
#
# Copy a resolved framework into the bundle if necessary.  Copy is not
# necessary if the resolved_item is "the same as" the
# resolved_embedded_item.
#
# By default, BU_COPY_FULL_FRAMEWORK_CONTENTS is not set.  If you want
# full frameworks embedded in your bundles, set
# BU_COPY_FULL_FRAMEWORK_CONTENTS to ON before calling fixup_bundle.  By
# default, COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE copies the framework
# dylib itself plus the framework Resources directory.
#
# ::
#
#   FIXUP_BUNDLE_ITEM(<resolved_embedded_item> <exepath> <dirs>)
#
# Get the direct/non-system prerequisites of the resolved embedded item.
# For each prerequisite, change the way it is referenced to the value of
# the _EMBEDDED_ITEM keyed variable for that prerequisite.  (Most likely
# changing to an "@executable_path" style reference.)
#
# This function requires that the resolved_embedded_item be "inside" the
# bundle already.  In other words, if you pass plugins to fixup_bundle
# as the libs parameter, you should install them or copy them into the
# bundle before calling fixup_bundle.  The "libs" parameter is a list of
# libraries that must be fixed up, but that cannot be determined by
# otool output analysis.  (i.e., plugins)
#
# Also, change the id of the item being fixed up to its own
# _EMBEDDED_ITEM value.
#
# Accumulate changes in a local variable and make *one* call to
# install_name_tool at the end of the function with all the changes at
# once.
#
# If the BU_CHMOD_BUNDLE_ITEMS variable is set then bundle items will be
# marked writable before install_name_tool tries to change them.
#
# ::
#
#   VERIFY_BUNDLE_PREREQUISITES(<bundle> <result_var> <info_var>)
#
# Verifies that the sum of all prerequisites of all files inside the
# bundle are contained within the bundle or are "system" libraries,
# presumed to exist everywhere.
#
# ::
#
#   VERIFY_BUNDLE_SYMLINKS(<bundle> <result_var> <info_var>)
#
# Verifies that any symlinks found in the bundle point to other files
# that are already also in the bundle...  Anything that points to an
# external file causes this function to fail the verification.

#=============================================================================
# Copyright 2008-2009 Kitware, Inc.
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
#
# This software is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the License for more information.
#=============================================================================
# (To distribute this file outside of CMake, substitute the full
#  License text for the above reference.)

# The functions defined in this file depend on the get_prerequisites function
# (and possibly others) found in:
#
get_filename_component(BundleUtilities_cmake_dir "${CMAKE_CURRENT_LIST_FILE}" PATH)
include("${BundleUtilities_cmake_dir}/GetPrerequisites.cmake")

function(get_bundle_main_executable bundle result_var)
  set(result "error: '${bundle}/Contents/Info.plist' file does not exist")

  if(EXISTS "${bundle}/Contents/Info.plist")
    set(result "error: no CFBundleExecutable in '${bundle}/Contents/Info.plist' file")
    set(line_is_main_executable 0)
    set(bundle_executable "")

    # Read Info.plist as a list of lines:
    #
    set(eol_char "E")
    file(READ "${bundle}/Contents/Info.plist" info_plist)
    string(REPLACE ";" "\\;" info_plist "${info_plist}")
    string(REPLACE "\n" "${eol_char};" info_plist "${info_plist}")
    string(REPLACE "\r" "${eol_char};" info_plist "${info_plist}")

    # Scan the lines for "<key>CFBundleExecutable</key>" - the line after that
    # is the name of the main executable.
    #
    foreach(line ${info_plist})
      if(line_is_main_executable)
        string(REGEX REPLACE "^.*<string>(.*)</string>.*$" "\\1" bundle_executable "${line}")
        break()
      endif()

      if(line MATCHES "<key>CFBundleExecutable</key>")
        set(line_is_main_executable 1)
      endif()
    endforeach()

    if(NOT "${bundle_executable}" STREQUAL "")
      if(EXISTS "${bundle}/Contents/MacOS/${bundle_executable}")
        set(result "${bundle}/Contents/MacOS/${bundle_executable}")
      else()

        # Ultimate goal:
        # If not in "Contents/MacOS" then scan the bundle for matching files. If
        # there is only one executable file that matches, then use it, otherwise
        # it's an error...
        #
        #file(GLOB_RECURSE file_list "${bundle}/${bundle_executable}")

        # But for now, pragmatically, it's an error. Expect the main executable
        # for the bundle to be in Contents/MacOS, it's an error if it's not:
        #
        set(result "error: '${bundle}/Contents/MacOS/${bundle_executable}' does not exist")
      endif()
    endif()
  else()
    #
    # More inclusive technique... (This one would work on Windows and Linux
    # too, if a developer followed the typical Mac bundle naming convention...)
    #
    # If there is no Info.plist file, try to find an executable with the same
    # base name as the .app directory:
    #
  endif()

  set(${result_var} "${result}" PARENT_SCOPE)
endfunction()

function(get_dotapp_dir exe dotapp_dir_var)
  set(s "${exe}")

  if(s MATCHES "/.*\\.app/")
    # If there is a ".app" parent directory,
    # ascend until we hit it:
    #   (typical of a Mac bundle executable)
    #
    set(done 0)
    while(NOT ${done})
      get_filename_component(snamewe "${s}" NAME_WE)
      get_filename_component(sname "${s}" NAME)
      get_filename_component(sdir "${s}" PATH)
      set(s "${sdir}")
      if(sname MATCHES "\\.app$")
        set(done 1)
        set(dotapp_dir "${sdir}/${sname}")
      endif()
    endwhile()
  else()
    # Otherwise use a directory containing the exe
    #   (typical of a non-bundle executable on Mac, Windows or Linux)
    #
    is_file_executable("${s}" is_executable)
    if(is_executable)
      get_filename_component(sdir "${s}" PATH)
      set(dotapp_dir "${sdir}")
    else()
      set(dotapp_dir "${s}")
    endif()
  endif()

  set(${dotapp_dir_var} "${dotapp_dir}" PARENT_SCOPE)
endfunction()

function(get_bundle_and_executable app bundle_var executable_var valid_var)
  set(valid 0)

  if(EXISTS "${app}")
    # Is it a directory ending in .app?
    if(IS_DIRECTORY "${app}")
      if(app MATCHES "\\.app$")
        get_bundle_main_executable("${app}" executable)
        if(EXISTS "${app}" AND EXISTS "${executable}")
          set(${bundle_var} "${app}" PARENT_SCOPE)
          set(${executable_var} "${executable}" PARENT_SCOPE)
          set(valid 1)
          #message(STATUS "info: handled .app directory case...")
        else()
          message(STATUS "warning: *NOT* handled - .app directory case...")
        endif()
      else()
        message(STATUS "warning: *NOT* handled - directory but not .app case...")
      endif()
    else()
      # Is it an executable file?
      is_file_executable("${app}" is_executable)
      if(is_executable)
        get_dotapp_dir("${app}" dotapp_dir)
        if(EXISTS "${dotapp_dir}")
          set(${bundle_var} "${dotapp_dir}" PARENT_SCOPE)
          set(${executable_var} "${app}" PARENT_SCOPE)
          set(valid 1)
          #message(STATUS "info: handled executable file in .app dir case...")
        else()
          get_filename_component(app_dir "${app}" PATH)
          set(${bundle_var} "${app_dir}" PARENT_SCOPE)
          set(${executable_var} "${app}" PARENT_SCOPE)
          set(valid 1)
          #message(STATUS "info: handled executable file in any dir case...")
        endif()
      else()
        message(STATUS "warning: *NOT* handled - not .app dir, not executable file...")
      endif()
    endif()
  else()
    message(STATUS "warning: *NOT* handled - directory/file does not exist...")
  endif()

  if(NOT valid)
    set(${bundle_var} "error: not a bundle" PARENT_SCOPE)
    set(${executable_var} "error: not a bundle" PARENT_SCOPE)
  endif()

  set(${valid_var} ${valid} PARENT_SCOPE)
endfunction()

function(get_bundle_all_executables bundle exes_var)
  set(exes "")

  if(UNIX)
    find_program(find_cmd "find")
    mark_as_advanced(find_cmd)
  endif()

  # find command is much quicker than checking every file one by one on Unix
  # which can take long time for large bundles, and since anyway we expect
  # executable to have execute flag set we can narrow the list much quicker.
  if(find_cmd)
    execute_process(COMMAND "${find_cmd}" "${bundle}"
      -type f \( -perm -0100 -o -perm -0010 -o -perm -0001 \)
      OUTPUT_VARIABLE file_list
      OUTPUT_STRIP_TRAILING_WHITESPACE
      )
    string(REPLACE "\n" ";" file_list "${file_list}")
  else()
    file(GLOB_RECURSE file_list "${bundle}/*")
  endif()

  foreach(f ${file_list})
    is_file_executable("${f}" is_executable)
    if(is_executable)
      set(exes ${exes} "${f}")
    endif()
  endforeach()

  set(${exes_var} "${exes}" PARENT_SCOPE)
endfunction()

function(get_item_rpaths item rpaths_var)
  if(APPLE)
    find_program(otool_cmd "otool")
    mark_as_advanced(otool_cmd)
  endif()

  if(otool_cmd)
    execute_process(
      COMMAND "${otool_cmd}" -l "${item}"
      OUTPUT_VARIABLE load_cmds_ov
      )
    string(REGEX REPLACE "[^\n]+cmd LC_RPATH\n[^\n]+\n[^\n]+path ([^\n]+) \\(offset[^\n]+\n" "rpath \\1\n" load_cmds_ov "${load_cmds_ov}")
    string(REGEX MATCHALL "rpath [^\n]+" load_cmds_ov "${load_cmds_ov}")
    string(REGEX REPLACE "rpath " "" load_cmds_ov "${load_cmds_ov}")
    if(load_cmds_ov)
      gp_append_unique(${rpaths_var} "${load_cmds_ov}")
    endif()
  endif()

  set(${rpaths_var} ${${rpaths_var}} PARENT_SCOPE)
endfunction()

function(get_item_key item key_var)
  get_filename_component(item_name "${item}" NAME)
  if(WIN32)
    string(TOLOWER "${item_name}" item_name)
  endif()
  string(REPLACE "." "_" ${key_var} "${item_name}")
  set(${key_var} ${${key_var}} PARENT_SCOPE)
endfunction()

function(clear_bundle_keys keys_var)
  foreach(key ${${keys_var}})
    set(${key}_ITEM PARENT_SCOPE)
    set(${key}_RESOLVED_ITEM PARENT_SCOPE)
    set(${key}_DEFAULT_EMBEDDED_PATH PARENT_SCOPE)
    set(${key}_EMBEDDED_ITEM PARENT_SCOPE)
    set(${key}_RESOLVED_EMBEDDED_ITEM PARENT_SCOPE)
    set(${key}_COPYFLAG PARENT_SCOPE)
    set(${key}_RPATHS PARENT_SCOPE)
  endforeach()
  set(${keys_var} PARENT_SCOPE)
endfunction()

function(set_bundle_key_values keys_var context item exepath dirs copyflag)
  if(ARGC GREATER 6)
    set(rpaths "${ARGV6}")
  else()
    set(rpaths "")
  endif()
  get_filename_component(item_name "${item}" NAME)

  get_item_key("${item}" key)

  list(LENGTH ${keys_var} length_before)
  gp_append_unique(${keys_var} "${key}")
  list(LENGTH ${keys_var} length_after)

  if(NOT length_before EQUAL length_after)
    gp_resolve_item("${context}" "${item}" "${exepath}" "${dirs}" resolved_item "${rpaths}")

    gp_item_default_embedded_path("${item}" default_embedded_path)

    get_item_rpaths("${resolved_item}" item_rpaths)

    if(item MATCHES "[^/]+\\.framework/")
      # For frameworks, construct the name under the embedded path from the
      # opening "${item_name}.framework/" to the closing "/${item_name}":
      #
      string(REGEX REPLACE "^.*(${item_name}.framework/.*/?${item_name}).*$" "${default_embedded_path}/\\1" embedded_item "${item}")
    else()
      # For other items, just use the same name as the original, but in the
      # embedded path:
      #
      set(embedded_item "${default_embedded_path}/${item_name}")
    endif()

    # Replace @executable_path and resolve ".." references:
    #
    string(REPLACE "@executable_path" "${exepath}" resolved_embedded_item "${embedded_item}")
    get_filename_component(resolved_embedded_item "${resolved_embedded_item}" ABSOLUTE)

    # *But* -- if we are not copying, then force resolved_embedded_item to be
    # the same as resolved_item. In the case of multiple executables in the
    # original bundle, using the default_embedded_path results in looking for
    # the resolved executable next to the main bundle executable. This is here
    # so that exes in the other sibling directories (like "bin") get fixed up
    # properly...
    #
    if(NOT copyflag)
      set(resolved_embedded_item "${resolved_item}")
    endif()

    set(${keys_var} ${${keys_var}} PARENT_SCOPE)
    set(${key}_ITEM "${item}" PARENT_SCOPE)
    set(${key}_RESOLVED_ITEM "${resolved_item}" PARENT_SCOPE)
    set(${key}_DEFAULT_EMBEDDED_PATH "${default_embedded_path}" PARENT_SCOPE)
    set(${key}_EMBEDDED_ITEM "${embedded_item}" PARENT_SCOPE)
    set(${key}_RESOLVED_EMBEDDED_ITEM "${resolved_embedded_item}" PARENT_SCOPE)
    set(${key}_COPYFLAG "${copyflag}" PARENT_SCOPE)
    set(${key}_RPATHS "${item_rpaths}" PARENT_SCOPE)
    set(${key}_RDEP_RPATHS "${rpaths}" PARENT_SCOPE)
  else()
    #message("warning: item key '${key}' already in the list, subsequent references assumed identical to first")
  endif()
endfunction()

function(get_bundle_keys app libs dirs keys_var)
  set(${keys_var} PARENT_SCOPE)

  get_bundle_and_executable("${app}" bundle executable valid)
  if(valid)
    # Always use the exepath of the main bundle executable for @executable_path
    # replacements:
    #
    get_filename_component(exepath "${executable}" PATH)

    # But do fixups on all executables in the bundle:
    #
    get_bundle_all_executables("${bundle}" exes)

    # Set keys for main executable first:
    #
    set_bundle_key_values(${keys_var} "${executable}" "${executable}" "${exepath}" "${dirs}" 0)

    # Get rpaths specified by main executable:
    #
    get_item_key("${executable}" executable_key)
    set(main_rpaths "${${executable_key}_RPATHS}")

    # For each extra lib, accumulate a key as well and then also accumulate
    # any of its prerequisites. (Extra libs are typically dynamically loaded
    # plugins: libraries that are prerequisites for full runtime functionality
    # but that do not show up in otool -L output...)
    #
    foreach(lib ${libs})
      set_bundle_key_values(${keys_var} "${lib}" "${lib}" "${exepath}" "${dirs}" 0 "${main_rpaths}")

      set(prereqs "")
      get_prerequisites("${lib}" prereqs 1 1 "${exepath}" "${dirs}" "${main_rpaths}")
      foreach(pr ${prereqs})
        set_bundle_key_values(${keys_var} "${lib}" "${pr}" "${exepath}" "${dirs}" 1 "${main_rpaths}")
      endforeach()
    endforeach()

    # For each executable found in the bundle, accumulate keys as we go.
    # The list of keys should be complete when all prerequisites of all
    # binaries in the bundle have been analyzed.
    #
    foreach(exe ${exes})
      # Main executable is scanned first above:
      #
      if(NOT "${exe}" STREQUAL "${executable}")
        # Add the exe itself to the keys:
        #
        set_bundle_key_values(${keys_var} "${exe}" "${exe}" "${exepath}" "${dirs}" 0 "${main_rpaths}")

        # Get rpaths specified by executable:
        #
        get_item_key("${exe}" exe_key)
        set(exe_rpaths "${main_rpaths}" "${${exe_key}_RPATHS}")
      else()
        set(exe_rpaths "${main_rpaths}")
      endif()

      # Add each prerequisite to the keys:
      #
      set(prereqs "")
      get_prerequisites("${exe}" prereqs 1 1 "${exepath}" "${dirs}" "${exe_rpaths}")
      foreach(pr ${prereqs})
        set_bundle_key_values(${keys_var} "${exe}" "${pr}" "${exepath}" "${dirs}" 1 "${exe_rpaths}")
      endforeach()
    endforeach()

    # Propagate values to caller's scope:
    #
    set(${keys_var} ${${keys_var}} PARENT_SCOPE)
    foreach(key ${${keys_var}})
      set(${key}_ITEM "${${key}_ITEM}" PARENT_SCOPE)
      set(${key}_RESOLVED_ITEM "${${key}_RESOLVED_ITEM}" PARENT_SCOPE)
      set(${key}_DEFAULT_EMBEDDED_PATH "${${key}_DEFAULT_EMBEDDED_PATH}" PARENT_SCOPE)
      set(${key}_EMBEDDED_ITEM "${${key}_EMBEDDED_ITEM}" PARENT_SCOPE)
      set(${key}_RESOLVED_EMBEDDED_ITEM "${${key}_RESOLVED_EMBEDDED_ITEM}" PARENT_SCOPE)
      set(${key}_COPYFLAG "${${key}_COPYFLAG}" PARENT_SCOPE)
      set(${key}_RPATHS "${${key}_RPATHS}" PARENT_SCOPE)
      set(${key}_RDEP_RPATHS "${${key}_RDEP_RPATHS}" PARENT_SCOPE)
    endforeach()
  endif()
endfunction()

function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item)
  if(WIN32)
    # ignore case on Windows
    string(TOLOWER "${resolved_item}" resolved_item_compare)
    string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare)
  else()
    set(resolved_item_compare "${resolved_item}")
    set(resolved_embedded_item_compare "${resolved_embedded_item}")
  endif()

  if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}")
    message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...")
  else()
    #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}")
    execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}")
    if(UNIX AND NOT APPLE)
      file(RPATH_REMOVE FILE "${resolved_embedded_item}")
    endif()
  endif()

endfunction()

function(copy_resolved_framework_into_bundle resolved_item resolved_embedded_item)
  if(WIN32)
    # ignore case on Windows
    string(TOLOWER "${resolved_item}" resolved_item_compare)
    string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare)
  else()
    set(resolved_item_compare "${resolved_item}")
    set(resolved_embedded_item_compare "${resolved_embedded_item}")
  endif()

  if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}")
    message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...")
  else()
    if(BU_COPY_FULL_FRAMEWORK_CONTENTS)
      # Full Framework (everything):
      get_filename_component(resolved_dir "${resolved_item}" PATH)
      get_filename_component(resolved_dir "${resolved_dir}/../.." ABSOLUTE)
      get_filename_component(resolved_embedded_dir "${resolved_embedded_item}" PATH)
      get_filename_component(resolved_embedded_dir "${resolved_embedded_dir}/../.." ABSOLUTE)
      #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_dir}' '${resolved_embedded_dir}'")
      execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_dir}" "${resolved_embedded_dir}")
    else()
      # Framework lib itself:
      #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}")
      execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}")

      # Plus Resources, if they exist:
      string(REGEX REPLACE "^(.*)/[^/]+$" "\\1/Resources" resolved_resources "${resolved_item}")
      string(REGEX REPLACE "^(.*)/[^/]+$" "\\1/Resources" resolved_embedded_resources "${resolved_embedded_item}")
      if(EXISTS "${resolved_resources}")
        #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_resources}' '${resolved_embedded_resources}'")
        execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_resources}" "${resolved_embedded_resources}")
      endif()

      # Some frameworks e.g. Qt put Info.plist in wrong place, so when it is
      # missing in resources, copy it from other well known incorrect locations:
      if(NOT EXISTS "${resolved_resources}/Info.plist")
        # Check for Contents/Info.plist in framework root (older Qt SDK):
        string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Contents/Info.plist" resolved_info_plist "${resolved_item}")
        string(REGEX REPLACE "^(.*)/[^/]+$" "\\1/Resources/Info.plist" resolved_embedded_info_plist "${resolved_embedded_item}")
        if(EXISTS "${resolved_info_plist}")
          #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_info_plist}' '${resolved_embedded_info_plist}'")
          execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_info_plist}" "${resolved_embedded_info_plist}")
        endif()
      endif()

      # Check if framework is versioned and fix it layout
      string(REGEX REPLACE "^.*/([^/]+)/[^/]+$" "\\1" resolved_embedded_version "${resolved_embedded_item}")
      string(REGEX REPLACE "^(.*)/[^/]+/[^/]+$" "\\1" resolved_embedded_versions "${resolved_embedded_item}")
      string(REGEX REPLACE "^.*/([^/]+)/[^/]+/[^/]+$" "\\1" resolved_embedded_versions_basename "${resolved_embedded_item}")
      if(resolved_embedded_versions_basename STREQUAL "Versions")
        # Ensure Current symlink points to the framework version
        if(NOT EXISTS "${resolved_embedded_versions}/Current")
          execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink "${resolved_embedded_version}" "${resolved_embedded_versions}/Current")
        endif()
        # Restore symlinks in framework root pointing to current framework
        # binary and resources:
        string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1" resolved_embedded_root "${resolved_embedded_item}")
        string(REGEX REPLACE "^.*/([^/]+)$" "\\1" resolved_embedded_item_basename "${resolved_embedded_item}")
        if(NOT EXISTS "${resolved_embedded_root}/${resolved_embedded_item_basename}")
          execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/${resolved_embedded_item_basename}" "${resolved_embedded_root}/${resolved_embedded_item_basename}")
        endif()
        if(NOT EXISTS "${resolved_embedded_root}/Resources")
          execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/Resources" "${resolved_embedded_root}/Resources")
        endif()
      endif()
    endif()
    if(UNIX AND NOT APPLE)
      file(RPATH_REMOVE FILE "${resolved_embedded_item}")
    endif()
  endif()

endfunction()

function(fixup_bundle_item resolved_embedded_item exepath dirs)
  # This item's key is "ikey":
  #
  get_item_key("${resolved_embedded_item}" ikey)

  # Ensure the item is "inside the .app bundle" -- it should not be fixed up if
  # it is not in the .app bundle... Otherwise, we'll modify files in the build
  # tree, or in other varied locations around the file system, with our call to
  # install_name_tool. Make sure that doesn't happen here:
  #
  get_dotapp_dir("${exepath}" exe_dotapp_dir)
  string(LENGTH "${exe_dotapp_dir}/" exe_dotapp_dir_length)
  string(LENGTH "${resolved_embedded_item}" resolved_embedded_item_length)
  set(path_too_short 0)
  set(is_embedded 0)
  if(${resolved_embedded_item_length} LESS ${exe_dotapp_dir_length})
    set(path_too_short 1)
  endif()
  if(NOT path_too_short)
    string(SUBSTRING "${resolved_embedded_item}" 0 ${exe_dotapp_dir_length} item_substring)
    if("${exe_dotapp_dir}/" STREQUAL "${item_substring}")
      set(is_embedded 1)
    endif()
  endif()
  if(NOT is_embedded)
    message("  exe_dotapp_dir/='${exe_dotapp_dir}/'")
    message("  item_substring='${item_substring}'")
    message("  resolved_embedded_item='${resolved_embedded_item}'")
    message("")
    message("Install or copy the item into the bundle before calling fixup_bundle.")
    message("Or maybe there's a typo or incorrect path in one of the args to fixup_bundle?")
    message("")
    message(FATAL_ERROR "cannot fixup an item that is not in the bundle...")
  endif()

  set(rpaths "${${ikey}_RPATHS}" "${${ikey}_RDEP_RPATHS}")

  set(prereqs "")
  get_prerequisites("${resolved_embedded_item}" prereqs 1 0 "${exepath}" "${dirs}" "${rpaths}")

  set(changes "")

  foreach(pr ${prereqs})
    # Each referenced item's key is "rkey" in the loop:
    #
    get_item_key("${pr}" rkey)

    if(NOT "${${rkey}_EMBEDDED_ITEM}" STREQUAL "")
      set(changes ${changes} "-change" "${pr}" "${${rkey}_EMBEDDED_ITEM}")
    else()
      message("warning: unexpected reference to '${pr}'")
    endif()
  endforeach()

  if(BU_CHMOD_BUNDLE_ITEMS)
    execute_process(COMMAND chmod u+w "${resolved_embedded_item}")
  endif()

  # Only if install_name_tool supports -delete_rpath:
  #
  execute_process(COMMAND install_name_tool
    OUTPUT_VARIABLE install_name_tool_usage
    ERROR_VARIABLE  install_name_tool_usage
    )
  if(install_name_tool_usage MATCHES ".*-delete_rpath.*")
    foreach(rpath ${${ikey}_RPATHS})
      set(changes ${changes} -delete_rpath "${rpath}")
    endforeach()
  endif()

  if(${ikey}_EMBEDDED_ITEM)
    set(changes ${changes} -id "${${ikey}_EMBEDDED_ITEM}")
  endif()

  # Change this item's id and all of its references in one call
  # to install_name_tool:
  #
  if(changes)
    set(cmd install_name_tool ${changes} "${resolved_embedded_item}")
    execute_process(COMMAND ${cmd} RESULT_VARIABLE install_name_tool_result)
    if(NOT install_name_tool_result EQUAL 0)
      string(REPLACE ";" "' '" msg "'${cmd}'")
      message(FATAL_ERROR "Command failed:\n ${msg}")
    endif()
  endif()
endfunction()

function(fixup_bundle app libs dirs)
  message(STATUS "fixup_bundle")
  message(STATUS "  app='${app}'")
  message(STATUS "  libs='${libs}'")
  message(STATUS "  dirs='${dirs}'")

  get_bundle_and_executable("${app}" bundle executable valid)
  if(valid)
    get_filename_component(exepath "${executable}" PATH)

    message(STATUS "fixup_bundle: preparing...")
    get_bundle_keys("${app}" "${libs}" "${dirs}" keys)

    message(STATUS "fixup_bundle: copying...")
    list(LENGTH keys n)
    math(EXPR n ${n}*2)

    set(i 0)
    foreach(key ${keys})
      math(EXPR i ${i}+1)
      if(${${key}_COPYFLAG})
        message(STATUS "${i}/${n}: copying '${${key}_RESOLVED_ITEM}'")
      else()
        message(STATUS "${i}/${n}: *NOT* copying '${${key}_RESOLVED_ITEM}'")
      endif()

      set(show_status 0)
      if(show_status)
        message(STATUS "key='${key}'")
        message(STATUS "item='${${key}_ITEM}'")
        message(STATUS "resolved_item='${${key}_RESOLVED_ITEM}'")
        message(STATUS "default_embedded_path='${${key}_DEFAULT_EMBEDDED_PATH}'")
        message(STATUS "embedded_item='${${key}_EMBEDDED_ITEM}'")
        message(STATUS "resolved_embedded_item='${${key}_RESOLVED_EMBEDDED_ITEM}'")
        message(STATUS "copyflag='${${key}_COPYFLAG}'")
        message(STATUS "")
      endif()

      if(${${key}_COPYFLAG})
        set(item "${${key}_ITEM}")
        if(item MATCHES "[^/]+\\.framework/")
          copy_resolved_framework_into_bundle("${${key}_RESOLVED_ITEM}"
            "${${key}_RESOLVED_EMBEDDED_ITEM}")
        else()
          copy_resolved_item_into_bundle("${${key}_RESOLVED_ITEM}"
            "${${key}_RESOLVED_EMBEDDED_ITEM}")
        endif()
      endif()
    endforeach()

    message(STATUS "fixup_bundle: fixing...")
    foreach(key ${keys})
      math(EXPR i ${i}+1)
      if(APPLE)
        message(STATUS "${i}/${n}: fixing up '${${key}_RESOLVED_EMBEDDED_ITEM}'")
        fixup_bundle_item("${${key}_RESOLVED_EMBEDDED_ITEM}" "${exepath}" "${dirs}")
      else()
        message(STATUS "${i}/${n}: fix-up not required on this platform '${${key}_RESOLVED_EMBEDDED_ITEM}'")
      endif()
    endforeach()

    message(STATUS "fixup_bundle: cleaning up...")
    clear_bundle_keys(keys)

    message(STATUS "fixup_bundle: verifying...")
    verify_app("${app}")
  else()
    message(SEND_ERROR "error: fixup_bundle: not a valid bundle")
  endif()

  message(STATUS "fixup_bundle: done")
endfunction()

function(copy_and_fixup_bundle src dst libs dirs)
  execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${src}" "${dst}")
  fixup_bundle("${dst}" "${libs}" "${dirs}")
endfunction()

function(verify_bundle_prerequisites bundle result_var info_var)
  set(result 1)
  set(info "")
  set(count 0)

  get_bundle_main_executable("${bundle}" main_bundle_exe)

  get_bundle_all_executables("${bundle}" file_list)
  foreach(f ${file_list})
      get_filename_component(exepath "${f}" PATH)
      math(EXPR count "${count} + 1")

      message(STATUS "executable file ${count}: ${f}")

      set(prereqs "")
      get_prerequisites("${f}" prereqs 1 1 "${exepath}" "")

      # On the Mac,
      # "embedded" and "system" prerequisites are fine... anything else means
      # the bundle's prerequisites are not verified (i.e., the bundle is not
      # really "standalone")
      #
      # On Windows (and others? Linux/Unix/...?)
      # "local" and "system" prereqs are fine...
      #
      set(external_prereqs "")

      foreach(p ${prereqs})
        set(p_type "")
        gp_file_type("${f}" "${p}" p_type)

        if(APPLE)
          if(NOT "${p_type}" STREQUAL "embedded" AND NOT "${p_type}" STREQUAL "system")
            set(external_prereqs ${external_prereqs} "${p}")
          endif()
        else()
          if(NOT "${p_type}" STREQUAL "local" AND NOT "${p_type}" STREQUAL "system")
            set(external_prereqs ${external_prereqs} "${p}")
          endif()
        endif()
      endforeach()

      if(external_prereqs)
        # Found non-system/somehow-unacceptable prerequisites:
        set(result 0)
        set(info ${info} "external prerequisites found:\nf='${f}'\nexternal_prereqs='${external_prereqs}'\n")
      endif()
  endforeach()

  if(result)
    set(info "Verified ${count} executable files in '${bundle}'")
  endif()

  set(${result_var} "${result}" PARENT_SCOPE)
  set(${info_var} "${info}" PARENT_SCOPE)
endfunction()

function(verify_bundle_symlinks bundle result_var info_var)
  set(result 1)
  set(info "")
  set(count 0)

  # TODO: implement this function for real...
  # Right now, it is just a stub that verifies unconditionally...

  set(${result_var} "${result}" PARENT_SCOPE)
  set(${info_var} "${info}" PARENT_SCOPE)
endfunction()

function(verify_app app)
  set(verified 0)
  set(info "")

  get_bundle_and_executable("${app}" bundle executable valid)

  message(STATUS "===========================================================================")
  message(STATUS "Analyzing app='${app}'")
  message(STATUS "bundle='${bundle}'")
  message(STATUS "executable='${executable}'")
  message(STATUS "valid='${valid}'")

  # Verify that the bundle does not have any "external" prerequisites:
  #
  verify_bundle_prerequisites("${bundle}" verified info)
  message(STATUS "verified='${verified}'")
  message(STATUS "info='${info}'")
  message(STATUS "")

  if(verified)
    # Verify that the bundle does not have any symlinks to external files:
    #
    verify_bundle_symlinks("${bundle}" verified info)
    message(STATUS "verified='${verified}'")
    message(STATUS "info='${info}'")
    message(STATUS "")
  endif()

  if(NOT verified)
    message(FATAL_ERROR "error: verify_app failed")
  endif()
endfunction()


code:
# - Try to find FFTW3.
# Usage: find_package(FFTW3 [COMPONENTS [single double long-double threads]])
#
# Variables used by this module:
#  FFTW3_ROOT_DIR             - FFTW3 root directory
# Variables defined by this module:
#  FFTW3_FOUND                - system has FFTW3
#  FFTW3_INCLUDE_DIR          - the FFTW3 include directory (cached)
#  FFTW3_INCLUDE_DIRS         - the FFTW3 include directories
#                               (identical to FFTW3_INCLUDE_DIR)
#  FFTW3[FL]?_LIBRARY         - the FFTW3 library - double, single(F),
#                               long-double(L) precision (cached)
#  FFTW3[FL]?_THREADS_LIBRARY - the threaded FFTW3 library - double, single(F),
#                               long-double(L) precision (cached)
#  FFTW3_LIBRARIES            - list of all FFTW3 libraries found

# Copyright (C) 2009-2010
# ASTRON (Netherlands Institute for Radio Astronomy)
# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
#
# This file is part of the LOFAR software suite.
# The LOFAR software suite is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# The LOFAR software suite is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>.
#
# $Id: FindFFTW3.cmake 15918 2010-06-25 11:12:42Z loose $

# Use double precision by default.
if (FFTW3_FIND_COMPONENTS MATCHES "^$")
  set (_components double)
else ()
  set (_components ${FFTW3_FIND_COMPONENTS})
endif ()

# Loop over each component.
set (_libraries)
foreach (_comp ${_components})
  if (_comp STREQUAL "single")
    list (APPEND _libraries fftw3f)
  elseif (_comp STREQUAL "double")
    list (APPEND _libraries fftw3)
  elseif (_comp STREQUAL "long-double")
    list (APPEND _libraries fftw3l)
  elseif (_comp STREQUAL "threads")
    set (_use_threads ON)
  else (_comp STREQUAL "single")
    message (FATAL_ERROR "FindFFTW3: unknown component `${_comp}' specified. "
      "Valid components are `single', `double', `long-double', and `threads'.")
  endif (_comp STREQUAL "single")
endforeach (_comp ${_components})

# If using threads, we need to link against threaded libraries as well - except on Windows.
if (NOT WIN32 AND _use_threads)
  set (_thread_libs)
  foreach (_lib ${_libraries})
    list (APPEND _thread_libs ${_lib}_threads)
  endforeach (_lib ${_libraries})
  set (_libraries ${_thread_libs} ${_libraries})
endif (NOT WIN32 AND _use_threads)

# Keep a list of variable names that we need to pass on to
# find_package_handle_standard_args().
set (_check_list)

# Search for all requested libraries.
foreach (_lib ${_libraries})
  string (TOUPPER ${_lib} _LIB)
  find_library (${_LIB}_LIBRARY NAMES ${_lib} ${_lib}-3
    HINTS ${FFTW3_ROOT_DIR} PATH_SUFFIXES lib)
  mark_as_advanced (${_LIB}_LIBRARY)
  list (APPEND FFTW3_LIBRARIES ${${_LIB}_LIBRARY})
  list (APPEND _check_list ${_LIB}_LIBRARY)
endforeach (_lib ${_libraries})

# Search for the header file.
find_path (FFTW3_INCLUDE_DIR fftw3.h
  HINTS ${FFTW3_ROOT_DIR} PATH_SUFFIXES include)
mark_as_advanced (FFTW3_INCLUDE_DIR)
list(APPEND _check_list FFTW3_INCLUDE_DIR)

# Handle the QUIETLY and REQUIRED arguments and set FFTW_FOUND to TRUE if
# all listed variables are TRUE
include (FindPackageHandleStandardArgs)
find_package_handle_standard_args (FFTW3 DEFAULT_MSG ${_check_list})

if (FFTW3_FOUND)
  set (FFTW3_INCLUDE_DIRS ${FFTW3_INCLUDE_DIR})
endif (FFTW3_FOUND)


code:
# - Extract information from a git-svn working copy
# The module defines the following variables:
#
# If the command line client executable is found two macros are defined:
# GitSubversion_WC_INFO(<dir> <var-prefix>)
# GitSubversion_WC_INFO extracts information of a subversion working copy at
# a given location. This macro defines the following variables:
#  <var-prefix>_WC_URL - url of the repository (at <dir>)
#  <var-prefix>_WC_ROOT - root url of the repository
#  <var-prefix>_WC_REVISION - current revision
#  <var-prefix>_WC_LAST_CHANGED_AUTHOR - author of last commit
#  <var-prefix>_WC_LAST_CHANGED_DATE - date of last commit
#  <var-prefix>_WC_LAST_CHANGED_REV - revision of last commit
#  <var-prefix>_WC_INFO - output of command `svn info <dir>'
# Example usage:
#  find_package(Subversion)
#  if(SUBVERSION_FOUND)
#    GitSubversion_WC_INFO(${PROJECT_SOURCE_DIR} Project)
#    message("Current revision is ${Project_WC_REVISION}")
#  endif()

find_package (Git)

if(GIT_FOUND)
  # the git-svn commands should be executed with the C locale, otherwise
  # the message (which are parsed) may be translated, Alex
  set(_Subversion_SAVED_LC_ALL "$ENV{LC_ALL}")
  set(ENV{LC_ALL} C)

  # execute_process(COMMAND ${Subversion_SVN_EXECUTABLE} --version
  #   OUTPUT_VARIABLE Subversion_VERSION_SVN
  #   OUTPUT_STRIP_TRAILING_WHITESPACE)

  # restore the previous LC_ALL
  set(ENV{LC_ALL} ${_Subversion_SAVED_LC_ALL})

  # string(REGEX REPLACE "^(.*\n)?svn, version ([.0-9]+).*"
  #   "\\2" Subversion_VERSION_SVN "${Subversion_VERSION_SVN}")

  macro(GitSubversion_WC_INFO dir prefix)
    # the subversion commands should be executed with the C locale, otherwise
    # the message (which are parsed) may be translated, Alex
    set(_Subversion_SAVED_LC_ALL "$ENV{LC_ALL}")
    set(ENV{LC_ALL} C)

    execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${dir}/.git svn info
      OUTPUT_VARIABLE ${prefix}_WC_INFO
      ERROR_VARIABLE Git_git_svn_info_error
      RESULT_VARIABLE Git_git_svn_info_result
      OUTPUT_STRIP_TRAILING_WHITESPACE)

    if(NOT ${Git_git_svn_info_result} EQUAL 0)
      message(SEND_ERROR "Command \"${GIT_EXECUTABLE} --git-dir=${dir}/.git svn info\" failed with output:\n${Git_git_svn_info_error}")
    else()

      string(REGEX REPLACE "^(.*\n)?URL: ([^\n]+).*"
        "\\2" ${prefix}_WC_URL "${${prefix}_WC_INFO}")
      string(REGEX REPLACE "^(.*\n)?Repository Root: ([^\n]+).*"
        "\\2" ${prefix}_WC_ROOT "${${prefix}_WC_INFO}")
      string(REGEX REPLACE "^(.*\n)?Revision: ([^\n]+).*"
        "\\2" ${prefix}_WC_REVISION "${${prefix}_WC_INFO}")
      string(REGEX REPLACE "^(.*\n)?Last Changed Author: ([^\n]+).*"
        "\\2" ${prefix}_WC_LAST_CHANGED_AUTHOR "${${prefix}_WC_INFO}")
      string(REGEX REPLACE "^(.*\n)?Last Changed Rev: ([^\n]+).*"
        "\\2" ${prefix}_WC_LAST_CHANGED_REV "${${prefix}_WC_INFO}")
      string(REGEX REPLACE "^(.*\n)?Last Changed Date: ([^\n]+).*"
        "\\2" ${prefix}_WC_LAST_CHANGED_DATE "${${prefix}_WC_INFO}")
    endif()

    # restore the previous LC_ALL
    set(ENV{LC_ALL} ${_Subversion_SAVED_LC_ALL})

  endmacro()

endif()