265 lines
7.9 KiB
C++
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;
|
|
}
|
|
}
|