2026-01-09 12:48:09 -05:00
|
|
|
/**
|
|
|
|
|
* @file BWFFile.cpp
|
|
|
|
|
* @brief Implementation of BWFFile class
|
2026-01-18 12:53:55 -06:00
|
|
|
*
|
2026-01-09 12:48:09 -05:00
|
|
|
* BWFFile is an enhanced, industry-standard version of a standard WAV file
|
|
|
|
|
*/
|
2026-01-01 15:12:24 -06:00
|
|
|
#include "BWFFile.h"
|
2026-01-18 12:53:55 -06:00
|
|
|
#include "JS8_Include/pimpl_impl.h"
|
2018-02-08 21:28:33 -05:00
|
|
|
|
|
|
|
|
#include <QAudioFormat>
|
|
|
|
|
#include <QDate>
|
2026-01-01 15:12:24 -06:00
|
|
|
#include <QDateTime>
|
|
|
|
|
#include <QString>
|
2018-02-08 21:28:33 -05:00
|
|
|
#include <QTime>
|
2024-10-21 09:41:19 -07:00
|
|
|
#include <QTimeZone>
|
2018-02-08 21:28:33 -05:00
|
|
|
#include <QUuid>
|
2026-01-01 15:12:24 -06:00
|
|
|
#include <qendian.h>
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <numeric>
|
2018-02-08 21:28:33 -05:00
|
|
|
|
|
|
|
|
#include "moc_BWFFile.cpp"
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
namespace {
|
|
|
|
|
// chunk descriptor
|
|
|
|
|
struct Desc {
|
|
|
|
|
Desc() : size_{0} {}
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
explicit Desc(char const *id, quint32 size = 0) : size_{size} { set(id); }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void set(char const *id = nullptr) {
|
|
|
|
|
if (id) {
|
|
|
|
|
auto end = reinterpret_cast<char const *>(::memchr(id, '\0', 4u));
|
|
|
|
|
auto len = end ? end - id : 4u;
|
|
|
|
|
::memcpy(id_.data(), id, len);
|
|
|
|
|
if (len < 4u) {
|
|
|
|
|
memset(id_.data() + len, ' ', 4u - len);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} else {
|
|
|
|
|
::memcpy(id_.data(), "JUNK", 4u);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void set(char const *id, quint32 size) {
|
|
|
|
|
set(id);
|
|
|
|
|
size_ = size;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
char *operator&() { return reinterpret_cast<char *>(this); }
|
|
|
|
|
char const *operator&() const {
|
|
|
|
|
return reinterpret_cast<char const *>(this);
|
|
|
|
|
}
|
2018-02-08 21:28:33 -05:00
|
|
|
|
|
|
|
|
std::array<char, 4> id_;
|
|
|
|
|
quint32 size_;
|
2026-01-18 12:53:55 -06:00
|
|
|
};
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
// "fmt " chunk contents
|
|
|
|
|
struct FormatChunk {
|
2018-02-08 21:28:33 -05:00
|
|
|
quint16 audio_format;
|
|
|
|
|
quint16 num_channels;
|
|
|
|
|
quint32 sample_rate;
|
|
|
|
|
quint32 byte_rate;
|
|
|
|
|
quint16 block_align;
|
|
|
|
|
quint16 bits_per_sample;
|
2026-01-18 12:53:55 -06:00
|
|
|
};
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
// "bext" chunk contents
|
|
|
|
|
struct BroadcastAudioExtension {
|
2018-02-08 21:28:33 -05:00
|
|
|
using Version = BWFFile::BextVersion;
|
|
|
|
|
using UMID = BWFFile::UMID;
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
BroadcastAudioExtension(Version version = Version::v_0)
|
|
|
|
|
: version_{static_cast<quint16>(version)}, umid_{{}} {
|
|
|
|
|
// set some sensible defaults for the "bext" fields
|
|
|
|
|
auto now = QDateTime::currentDateTimeUtc();
|
|
|
|
|
::memcpy(origination_date_,
|
|
|
|
|
now.date().toString("yyyy-MM-dd").toLocal8Bit().constData(),
|
|
|
|
|
sizeof origination_date_);
|
|
|
|
|
::memcpy(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);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_[];
|
2026-01-18 12:53:55 -06:00
|
|
|
};
|
|
|
|
|
} // namespace
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
class BWFFile::impl final {
|
|
|
|
|
public:
|
|
|
|
|
using FileError = QFile::FileError;
|
2026-01-09 12:48:09 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
impl(QAudioFormat const &format)
|
|
|
|
|
: header_dirty_{true}, format_{format}, header_length_{-1},
|
|
|
|
|
data_size_{-1}, error_{FileError::NoError} {}
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
|
|
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
BroadcastAudioExtension *bext() {
|
|
|
|
|
if (bext_.isEmpty()) // create a "bext" chunk in place
|
2018-02-08 21:28:33 -05:00
|
|
|
{
|
2026-01-18 12:53:55 -06:00
|
|
|
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_;
|
2026-02-05 19:38:26 -05:00
|
|
|
quint16 bits_per_sample_{0};
|
|
|
|
|
quint16 block_align_{0};
|
2026-01-18 12:53:55 -06:00
|
|
|
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);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
if (!memcmp(&wave_desc.id_, "fmt ", 4)) {
|
2026-02-05 19:38:26 -05:00
|
|
|
if (wave_size < sizeof(FormatChunk)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
FormatChunk fmt;
|
2026-02-05 19:38:26 -05:00
|
|
|
if (file_.read(reinterpret_cast<char*>(&fmt), sizeof fmt) != sizeof fmt) {
|
2026-01-18 12:53:55 -06:00
|
|
|
return false;
|
2026-02-05 19:38:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto audio_format = be ? qFromBigEndian<quint16>(fmt.audio_format)
|
|
|
|
|
: qFromLittleEndian<quint16>(fmt.audio_format);
|
|
|
|
|
if (audio_format != 0 && audio_format != 1 && audio_format != 3) {
|
2026-01-18 12:53:55 -06:00
|
|
|
return false; // not PCM nor undefined
|
2026-02-05 19:38:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto ch = be ? qFromBigEndian<quint16>(fmt.num_channels)
|
|
|
|
|
: qFromLittleEndian<quint16>(fmt.num_channels);
|
|
|
|
|
const auto rate = be ? qFromBigEndian<quint32>(fmt.sample_rate)
|
|
|
|
|
: qFromLittleEndian<quint32>(fmt.sample_rate);
|
|
|
|
|
const auto bits = be ? qFromBigEndian<quint16>(fmt.bits_per_sample)
|
|
|
|
|
: qFromLittleEndian<quint16>(fmt.bits_per_sample);
|
|
|
|
|
const auto ba = be ? qFromBigEndian<quint16>(fmt.block_align)
|
|
|
|
|
: qFromLittleEndian<quint16>(fmt.block_align);
|
|
|
|
|
|
|
|
|
|
if (audio_format == 3 && bits != 32) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bits_per_sample_ = bits;
|
|
|
|
|
block_align_ = ba;
|
|
|
|
|
|
|
|
|
|
format_.setChannelCount(ch);
|
|
|
|
|
format_.setSampleRate(rate);
|
|
|
|
|
|
|
|
|
|
switch (bits) {
|
|
|
|
|
case 8: format_.setSampleFormat(QAudioFormat::UInt8); break;
|
|
|
|
|
case 16: format_.setSampleFormat(QAudioFormat::Int16); break;
|
|
|
|
|
case 24: format_.setSampleFormat(QAudioFormat::Int32); break; // NOTE: we'll convert data to int32
|
|
|
|
|
case 32:
|
|
|
|
|
if (audio_format == 3) {
|
|
|
|
|
format_.setSampleFormat(QAudioFormat::Float); // 32-bit float WAV
|
|
|
|
|
} else {
|
|
|
|
|
format_.setSampleFormat(QAudioFormat::Int32); // 32-bit int PCM
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return false; // no 24-bit packed etc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bits != 24) {
|
|
|
|
|
if (ba != format_.bytesPerFrame())
|
|
|
|
|
return false;
|
|
|
|
|
} else {
|
|
|
|
|
// 24-bit packed: header must say 3 bytes/sample
|
|
|
|
|
if (ba != ch * 3)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} 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();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
if (!file_.seek(wave_offset + sizeof wave_desc +
|
|
|
|
|
(wave_size + 1) / 2 * 2))
|
|
|
|
|
return false;
|
|
|
|
|
wave_offset = file_.pos();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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 (!(file_.openMode() & WriteOnly))
|
|
|
|
|
return false;
|
|
|
|
|
if (!file_.seek(0))
|
|
|
|
|
return false;
|
|
|
|
|
header_length_ = 0;
|
|
|
|
|
// bool be {QSysInfo::BigEndian == format_.byteOrder ()};
|
|
|
|
|
bool be = false; // Should be OK if we only write RIFF files.
|
|
|
|
|
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.bytesPerSample() * 8);
|
|
|
|
|
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.bytesPerSample()) * 8;
|
|
|
|
|
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 {QSysInfo::BigEndian == format_.byteOrder ()};
|
|
|
|
|
bool be = false;
|
|
|
|
|
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_)) {
|
2018-02-08 21:28:33 -05:00
|
|
|
case BextVersion::v_0:
|
2026-01-18 12:53:55 -06:00
|
|
|
data->version_ = qToBigEndian<quint32>(data->version_);
|
|
|
|
|
// fall through
|
2018-02-08 21:28:33 -05:00
|
|
|
default:
|
2026-01-18 12:53:55 -06:00
|
|
|
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_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} 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_)) {
|
2018-02-08 21:28:33 -05:00
|
|
|
case BextVersion::v_0:
|
2026-01-18 12:53:55 -06:00
|
|
|
data->version_ = qToLittleEndian<quint32>(data->version_);
|
|
|
|
|
// fall through
|
2018-02-08 21:28:33 -05:00
|
|
|
default:
|
2026-01-18 12:53:55 -06:00
|
|
|
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_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// BWFFile implementation
|
|
|
|
|
//
|
2026-01-18 12:53:55 -06:00
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} 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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} else if (!(success = m_->file_.resize(m_->header_length_))) {
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} 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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} else if (!(success = m_->file_.resize(m_->header_length_))) {
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} 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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} else if (!(success = m_->file_.resize(m_->header_length_))) {
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
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());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
if (!success)
|
|
|
|
|
close();
|
|
|
|
|
return success;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
QAudioFormat const &BWFFile::format() const { return m_->format_; }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
auto BWFFile::list_info() -> InfoDictionary & {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
return m_->info_dictionary_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Broadcast Audio Extension fields
|
2026-01-18 12:53:55 -06:00
|
|
|
auto BWFFile::bext_version() const -> BextVersion {
|
|
|
|
|
return static_cast<BextVersion>(m_->bext() ? 0 : m_->bext()->version_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_version(BextVersion version) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->version_ = static_cast<quint16>(version);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
QByteArray BWFFile::bext_description() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return {};
|
|
|
|
|
return QByteArray::fromRawData(m_->bext()->description_,
|
|
|
|
|
strlen(m_->bext()->description_));
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_description(QByteArray const &description) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
::memcpy(m_->bext()->description_, description.constData(),
|
|
|
|
|
sizeof BroadcastAudioExtension::description_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
QByteArray BWFFile::bext_originator() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return {};
|
|
|
|
|
return QByteArray::fromRawData(m_->bext()->originator_,
|
|
|
|
|
strlen(m_->bext()->originator_));
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_originator(QByteArray const &originator) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
::memcpy(m_->bext()->originator_, originator.constData(),
|
|
|
|
|
sizeof BroadcastAudioExtension::originator_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
QByteArray BWFFile::bext_originator_reference() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return {};
|
|
|
|
|
return QByteArray::fromRawData(m_->bext()->originator_reference_,
|
|
|
|
|
strlen(m_->bext()->originator_reference_));
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_originator_reference(QByteArray const &reference) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
::memcpy(m_->bext()->originator_reference_, reference.constData(),
|
|
|
|
|
sizeof BroadcastAudioExtension::originator_reference_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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"),
|
|
|
|
|
QTimeZone::utc()};
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_origination_date_time(QDateTime const &dt) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
::memcpy(m_->bext()->origination_date_,
|
|
|
|
|
dt.date().toString("yyyy-MM-dd").toLocal8Bit().constData(),
|
|
|
|
|
sizeof BroadcastAudioExtension::origination_date_);
|
|
|
|
|
::memcpy(m_->bext()->origination_time_,
|
|
|
|
|
dt.time().toString("hh-mm-ss").toLocal8Bit().constData(),
|
|
|
|
|
sizeof BroadcastAudioExtension::origination_time_);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
quint64 BWFFile::bext_time_reference() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return 0;
|
|
|
|
|
return (quint64(m_->bext()->time_reference_high_) << 32) +
|
|
|
|
|
m_->bext()->time_reference_low_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
auto BWFFile::bext_umid() const -> UMID {
|
|
|
|
|
UMID umid{{'\0'}};
|
|
|
|
|
if (m_->bext()) {
|
|
|
|
|
umid = m_->bext()->umid_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
return umid;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_umid(UMID const &umid) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->umid_ = umid;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
quint16 BWFFile::bext_loudness_value() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return 0;
|
|
|
|
|
return m_->bext()->loudness_value_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_loudness_value(quint16 value) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->loudness_value_ = value;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
quint16 BWFFile::bext_loudness_range() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return 0;
|
|
|
|
|
return m_->bext()->loudness_range_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_loudness_range(quint16 range) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->loudness_range_ = range;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
quint16 BWFFile::bext_max_true_peak_level() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return 0;
|
|
|
|
|
return m_->bext()->max_true_peak_level_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_max_true_peak_level(quint16 level) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->max_true_peak_level_ = level;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
quint16 BWFFile::bext_max_momentary_loudness() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return 0;
|
|
|
|
|
return m_->bext()->max_momentary_loudness_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_max_momentary_loudness(quint16 loudness) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->max_momentary_loudness_ = loudness;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
quint16 BWFFile::bext_max_short_term_loudness() const {
|
|
|
|
|
if (!m_->bext())
|
|
|
|
|
return 0;
|
|
|
|
|
return m_->bext()->max_short_term_loudness_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_max_short_term_loudness(quint16 loudness) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext()->max_short_term_loudness_ = loudness;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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));
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::bext_coding_history(QByteArray const &text) {
|
|
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
m_->bext(); // ensure we have a correctly
|
|
|
|
|
// initialized m_->bext_
|
|
|
|
|
auto end = static_cast<char const *>(
|
|
|
|
|
::memchr(text.constData(), '\0', size_t(text.size())));
|
|
|
|
|
auto length = end ? end - text.constData() : size_t(text.size());
|
|
|
|
|
m_->bext_.resize(sizeof(BroadcastAudioExtension) + length);
|
|
|
|
|
std::strncpy(m_->bext()->coding_history_, text.constData(), length);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} else {
|
|
|
|
|
if (FileError::NoError == m_->file_.error()) {
|
|
|
|
|
setErrorString("Unable to update file header");
|
|
|
|
|
m_->error_ = FileError::WriteError;
|
|
|
|
|
} else {
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
return success;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
if (success) {
|
|
|
|
|
success = m_->file_.resize(m_->header_length_ + m_->data_size_);
|
|
|
|
|
if (!success) {
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
m_->header_dirty_ = true;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
} else {
|
|
|
|
|
setErrorString("File not open");
|
|
|
|
|
m_->error_ = FileError::WriteError;
|
|
|
|
|
success = false;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
return success && QIODevice::reset();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
qint64 BWFFile::size() const {
|
|
|
|
|
return m_->data_size_ < 0 ? m_->file_.size() - m_->header_length_
|
|
|
|
|
: m_->data_size_;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::isSequential() const { return m_->file_.isSequential(); }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-02-05 19:38:26 -05:00
|
|
|
quint16 BWFFile::bitsPerSample() const { return m_->bits_per_sample_; }
|
|
|
|
|
quint16 BWFFile::blockAlign() const { return m_->block_align_; }
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::close() {
|
|
|
|
|
if (isOpen())
|
|
|
|
|
QIODevice::close();
|
|
|
|
|
if (m_->header_dirty_ || m_->data_size_ < 0) {
|
|
|
|
|
m_->update_header();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
if (m_->file_.isOpen()) {
|
|
|
|
|
m_->file_.close();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::seek(qint64 pos) {
|
|
|
|
|
if (pos < 0) {
|
|
|
|
|
setErrorString("Attempt to seek before beginning of data");
|
|
|
|
|
m_->error_ = FileError::PositionError;
|
|
|
|
|
return false;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
bool success = QIODevice::seek(pos);
|
|
|
|
|
if (success) {
|
|
|
|
|
success = m_->file_.seek(pos + m_->header_length_);
|
|
|
|
|
if (!success)
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
return success;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
qint64 BWFFile::readData(char *data, qint64 max_size) {
|
|
|
|
|
return m_->file_.read(data, max_size);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
return bytes;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// forward to QFile
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::copy(QString const &new_name) {
|
|
|
|
|
close();
|
|
|
|
|
return m_->file_.copy(new_name);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::exists() const { return m_->file_.exists(); }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::link(QString const &link_name) {
|
|
|
|
|
return m_->file_.link(link_name);
|
|
|
|
|
}
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::remove() {
|
|
|
|
|
close();
|
|
|
|
|
return m_->file_.remove();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::rename(QString const &new_name) {
|
|
|
|
|
close();
|
|
|
|
|
return m_->file_.rename(new_name);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::setFileName(QString const &name) { m_->file_.setFileName(name); }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
QString BWFFile::symLinkTarget() const { return m_->file_.symLinkTarget(); }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
QString BWFFile::fileName() const { return m_->file_.fileName(); }
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
auto BWFFile::permissions() const -> Permissions {
|
|
|
|
|
return m_->file_.permissions();
|
|
|
|
|
}
|
2018-02-08 21:28:33 -05:00
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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);
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
m_->data_size_ = -1;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
2026-01-18 12:53:55 -06:00
|
|
|
m_->header_dirty_ = true;
|
|
|
|
|
return result;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::setPermissions(Permissions permissions) {
|
|
|
|
|
bool success = m_->file_.setPermissions(permissions);
|
|
|
|
|
if (!success)
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
|
|
|
|
return success;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
auto BWFFile::error() const -> FileError {
|
|
|
|
|
return m_->error_ != FileError::NoError ? m_->error_ : m_->file_.error();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::flush() {
|
|
|
|
|
bool success = m_->file_.flush();
|
|
|
|
|
if (!success)
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
|
|
|
|
return success;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
int BWFFile::handle() const {
|
|
|
|
|
int h{m_->file_.handle()};
|
|
|
|
|
if (h < 0)
|
|
|
|
|
const_cast<BWFFile *>(this)->setErrorString(m_->file_.errorString());
|
|
|
|
|
return h;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
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;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
bool BWFFile::unmap(uchar *address) {
|
|
|
|
|
bool success = m_->file_.unmap(address);
|
|
|
|
|
if (!success)
|
|
|
|
|
setErrorString(m_->file_.errorString());
|
|
|
|
|
return success;
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:53:55 -06:00
|
|
|
void BWFFile::unsetError() {
|
|
|
|
|
m_->error_ = FileError::NoError;
|
|
|
|
|
m_->file_.unsetError();
|
2018-02-08 21:28:33 -05:00
|
|
|
}
|