xybrid/xybrid/data/sample.cpp

265 lines
7.9 KiB
C++

#include "sample.h"
using namespace Xybrid::Data;
#include "data/project.h"
#include <deque>
#include <QCborValue>
#include <QCborMap>
#include <QCborArray>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QFileInfo>
#include <QAudioDecoder>
#include <QAudioFormat>
#include <QProcess>
#include <QDataStream>
#include <QEventLoop>
#include<QDebug>
#define qs QStringLiteral
#ifdef Q_OS_MAC
#define FFMPEG "/usr/local/bin/ffmpeg"
#else
#define FFMPEG "ffmpeg"
#endif
std::array<float, 2> Sample::plotBetween(size_t ch, size_t start, size_t end) const {
if (end < start) end = start;
if (ch >= 2 || start >= data[ch].size()) return {0, 0};
end = std::min(end, data[ch].size());
float mx = -100;
float mn = 100;
for (size_t i = start; i <= end; i++) {
auto v = data[ch][i];
mx = std::max(mx, v);
mn = std::min(mn, v);
}
return {mn, mx};
}
QCborMap Sample::toCbor() const {
QCborMap m;
m[qs("name")] = name;
m[qs("rate")] = sampleRate;
{
QCborArray ch;
auto n = static_cast<size_t>(numChannels());
for (size_t i = 0; i < n; i++) {
ch[static_cast<qsizetype>(i)] = QByteArray(reinterpret_cast<const char*>(data[i].data()), static_cast<int>(data[i].size() * sizeof(data[i][0])));
}
m[qs("channels")] = ch;
}
return m;
}
std::shared_ptr<Sample> Sample::fromCbor(const QCborMap& m, QUuid uuid) {
auto smp = std::make_shared<Sample>();
smp->uuid = uuid;
smp->name = m.value("name").toString();
smp->sampleRate = static_cast<int>(m.value("rate").toInteger(48000));
auto ch = m.value("channels").toArray();
auto s = static_cast<size_t>(ch.size());
for (size_t i = 0; i < s; i++) {
auto c = ch[static_cast<qint64>(i)].toByteArray();
auto bs = static_cast<size_t>(c.size());
smp->data[i].resize(bs / sizeof(*smp->data[i].begin()));
memcpy(smp->data[i].data(), c.constData(), bs);
}
return smp;
}
std::shared_ptr<Sample> Sample::fromCbor(const QCborValue& m, QUuid uuid) { return fromCbor(m.toMap(), uuid); }
bool Sample::changeUuid(QUuid newUuid) {
if (!project) return false;
//if (!project->samples.contains(uuid)) return false;
if (project->samples.contains(newUuid)) return false;
auto ptr = this->shared_from_this();
project->samples.remove(uuid);
uuid = newUuid;
project->samples.insert(uuid, ptr);
return true;
}
void Sample::newUuid() { changeUuid(QUuid::createUuid()); }
#ifdef OLD_SAMPLE_IMPORT
namespace {
bool blah [[maybe_unused]];
template<typename T> void insertbuf(std::shared_ptr<Sample> smp, const T* data, size_t len, size_t channels) {
for (size_t i = 0; i < len; i++) {
auto tch = i % channels;
if (tch < 2) smp->data[tch].push_back(static_cast<float>(data[i]) / std::numeric_limits<T>::max());
}
}
template<> void insertbuf(std::shared_ptr<Sample> smp, const float* data, size_t len, size_t channels) {
for (size_t i = 0; i < len; i++) {
auto tch = i % channels;
if (tch < 2) smp->data[tch].push_back(data[i]);
}
}
}
std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
QAudioFormat ifmt;
ifmt.setSampleType(QAudioFormat::SignedInt);
ifmt.setSampleSize(16);
auto dec = std::make_shared<QAudioDecoder>();
dec->setAudioFormat(ifmt);
dec->setSourceFilename(fileName);
std::deque<QAudioBuffer> bufs;
QEventLoop loop;
QObject::connect(dec.get(), &QAudioDecoder::bufferReady, &loop, [&bufs, dec] { bufs.push_back(dec->read()); });
QObject::connect(dec.get(), &QAudioDecoder::finished, &loop, [&loop] { loop.exit(); });
QObject::connect(dec.get(), static_cast<void(QAudioDecoder::*)(QAudioDecoder::Error)>(&QAudioDecoder::error), &loop, [&loop] { loop.exit(); });
dec->start();
loop.exec();
if (dec->error()) {
qDebug() << "sample decode error:" << dec->errorString();
return nullptr; // errored
}
if (bufs.empty()) return nullptr; // no sample data
auto fmt = bufs.front().format();
size_t channels = static_cast<size_t>(fmt.channelCount());
if (channels == 0) return nullptr; // zero channels means no sample data
size_t len = 0;
for (auto b : bufs) len += static_cast<size_t>(b.frameCount());
qDebug() << "format:" << fmt;
//qDebug() << "total length:" << len;
auto smp = std::make_shared<Sample>();
if (channels >= 1) smp->data[0].reserve(len);
if (channels >= 2) smp->data[1].reserve(len);
auto st = fmt.sampleType();
auto sz = fmt.sampleSize();
if (st == QAudioFormat::SignedInt) {
if (sz == 16) for (auto b : bufs) insertbuf(smp, b.constData<int16_t>(), static_cast<size_t>(b.sampleCount()), channels);
else if (sz == 32) for (auto b : bufs) insertbuf(smp, b.constData<int32_t>(), static_cast<size_t>(b.sampleCount()), channels);
else return nullptr; // unsupported
} else if (st == QAudioFormat::Float) {
if (sz == 32) for (auto b : bufs) insertbuf(smp, b.constData<float>(), static_cast<size_t>(b.sampleCount()), channels);
else return nullptr; // unsupported
} else return nullptr; // unsupported
smp->sampleRate = fmt.sampleRate();
smp->uuid = QUuid::createUuid();
smp->name = QFileInfo(fileName).baseName();
return smp;
}
#else
std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
QJsonObject info;
{
// get stream info
QProcess probe;
QStringList param;
param << "-v" << "quiet" << "-show_streams" << "-select_streams" << "a" << "-of" << "json";
param << fileName;
#ifdef Q_OS_MAC
#define FFPROBE "/usr/local/bin/ffprobe"
#else
#define FFPROBE "ffprobe"
#endif
probe.start(FFPROBE, param);
if (!probe.waitForFinished()) {
qCritical() << (probe.errorString());
}
auto mystdout = probe.readAllStandardOutput();
auto mystderr = probe.readAllStandardError();
auto doc = QJsonDocument::fromJson(mystdout);
info = doc.object()["streams"].toArray().first().toObject();
}
if (!info.contains("sample_rate") || !info.contains("channels")) return nullptr; // no/invalid audio streams
int channels = info["channels"].toInt();
int sampleRate = info["sample_rate"].toString().toInt(); // for some reason ffprobe stores this as a string??
auto smp = std::make_shared<Sample>();
smp->sampleRate = sampleRate;
// grab raw pcm_f32le via ffmpeg
QByteArray raw;
{
QProcess dec;
QStringList param;
param << "-i" << fileName << "-f" << "f32le" << "-acodec" << "pcm_f32le" << "-";
dec.start(FFMPEG, param);
dec.waitForFinished();
raw = dec.readAllStandardOutput();
}
// pre-size sample data buffers
auto len = static_cast<size_t>((raw.length() / channels) / 4);
smp->data[0].reserve(len);
if (channels > 1) smp->data[1].reserve(len);
// read raw bytes into channels
QDataStream r(&raw, QIODevice::ReadOnly);
size_t chs = static_cast<size_t>(channels);
size_t ch = chs;
float f;
while (!r.atEnd()) {
r.readRawData(reinterpret_cast<char*>(&f), sizeof(f));
ch = (ch+1) % chs;
if (ch < 2) smp->data[ch].push_back(f);
}
// add info
smp->uuid = QUuid::createUuid();
smp->name = QFileInfo(fileName).baseName();
return smp;
}
#endif
namespace {
bool exporting = false;
std::unordered_map<Sample*, bool> exportMap;
}
void Sample::startExport() {
exporting = true;
exportMap.reserve(16);
}
std::vector<std::shared_ptr<Sample>> Sample::finishExport() {
std::vector<std::shared_ptr<Sample>> v;
if (exporting) {
exporting = false;
v.reserve(exportMap.size());
for (auto it : exportMap) v.push_back(it.first->shared_from_this());
exportMap.clear();
}
return v;
}
void Sample::markForExport() {
if (exporting) {
exportMap[this] = true;
}
}