#include "sample.h" using namespace Xybrid::Data; #include "data/project.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define qs QStringLiteral #ifdef Q_OS_MAC #define FFMPEG "/usr/local/bin/ffmpeg" #else #define FFMPEG "ffmpeg" #endif std::array 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(numChannels()); for (size_t i = 0; i < n; i++) { ch[static_cast(i)] = QByteArray(reinterpret_cast(data[i].data()), static_cast(data[i].size() * sizeof(data[i][0]))); } m[qs("channels")] = ch; } return m; } std::shared_ptr Sample::fromCbor(const QCborMap& m, QUuid uuid) { auto smp = std::make_shared(); smp->uuid = uuid; smp->name = m.value("name").toString(); smp->sampleRate = static_cast(m.value("rate").toInteger(48000)); auto ch = m.value("channels").toArray(); auto s = static_cast(ch.size()); for (size_t i = 0; i < s; i++) { auto c = ch[static_cast(i)].toByteArray(); auto bs = static_cast(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::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 void insertbuf(std::shared_ptr 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(data[i]) / std::numeric_limits::max()); } } template<> void insertbuf(std::shared_ptr 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::fromFile(QString fileName) { QAudioFormat ifmt; ifmt.setSampleType(QAudioFormat::SignedInt); ifmt.setSampleSize(16); auto dec = std::make_shared(); dec->setAudioFormat(ifmt); dec->setSourceFilename(fileName); std::deque 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(&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(fmt.channelCount()); if (channels == 0) return nullptr; // zero channels means no sample data size_t len = 0; for (auto b : bufs) len += static_cast(b.frameCount()); qDebug() << "format:" << fmt; //qDebug() << "total length:" << len; auto smp = std::make_shared(); 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(), static_cast(b.sampleCount()), channels); else if (sz == 32) for (auto b : bufs) insertbuf(smp, b.constData(), static_cast(b.sampleCount()), channels); else return nullptr; // unsupported } else if (st == QAudioFormat::Float) { if (sz == 32) for (auto b : bufs) insertbuf(smp, b.constData(), static_cast(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::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(); 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((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(channels); size_t ch = chs; float f; while (!r.atEnd()) { r.readRawData(reinterpret_cast(&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 exportMap; } void Sample::startExport() { exporting = true; exportMap.reserve(16); } std::vector> Sample::finishExport() { std::vector> 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; } }