Restored recommendations based on acoustic similarities (using musicnn), fixes #301
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/IMusicNNEmbeddingExtractor.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include "core/XxHash3.hpp"
|
||||
|
||||
#if LMS_HAVE_ONNX_RUNTIME
|
||||
#include "musicnn/MusicNNEmbeddingExtractor.hpp"
|
||||
#endif
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
bool canExtractMusicNNEmbeddings()
|
||||
{
|
||||
#if LMS_HAVE_ONNX_RUNTIME
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<IMusicNNEmbeddingExtractor> createMusicNNEmbeddingExtractor([[maybe_unused]] const std::filesystem::path& modelPath, std::size_t maxPatchCount)
|
||||
{
|
||||
#if LMS_HAVE_ONNX_RUNTIME
|
||||
return std::make_unique<musicnn::MusicNNEmbeddingExtractor>(modelPath, maxPatchCount);
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string getMusicNNModelIdentifier(const std::filesystem::path& modelPath)
|
||||
{
|
||||
std::ifstream file{ modelPath, std::ios::binary };
|
||||
if (!file)
|
||||
return {};
|
||||
|
||||
core::XxHash3_64 hasher;
|
||||
constexpr std::size_t readBufSize{ 65536 };
|
||||
std::array<char, readBufSize> buf{};
|
||||
while (file.read(buf.data(), buf.size()) || file.gcount() > 0)
|
||||
hasher.update(std::as_bytes(std::span{ buf.data(), static_cast<std::size_t>(file.gcount()) }));
|
||||
|
||||
if (!file.eof())
|
||||
return {};
|
||||
|
||||
return std::to_string(hasher.digest());
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -38,4 +38,11 @@ namespace lms::audio
|
||||
throw Exception{ "Unhandled sample type" };
|
||||
}
|
||||
|
||||
namespace helpers
|
||||
{
|
||||
std::size_t sampleCountToByteCount(std::size_t sampleCount, PcmSampleType sampleType, unsigned channelCount)
|
||||
{
|
||||
return sampleCount * audio::getSampleSize(sampleType) * channelCount;
|
||||
}
|
||||
} // namespace helpers
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MelFilterBank.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio::features
|
||||
{
|
||||
float freqToMel(float freq)
|
||||
{
|
||||
return 2595.F * std::log10(1.0F + freq / 700.F);
|
||||
}
|
||||
|
||||
float melToFreq(float mel)
|
||||
{
|
||||
return 700.F * (std::pow(10.F, mel / 2595.F) - 1.F);
|
||||
}
|
||||
|
||||
MelFilterBank::MelFilterBank(std::vector<Filter>&& _filters, std::size_t binCount)
|
||||
: _filters{ std::move(_filters) }
|
||||
, _binCount{ binCount }
|
||||
{
|
||||
}
|
||||
|
||||
const MelFilterBank::Filter& MelFilterBank::getFilter(std::size_t m) const
|
||||
{
|
||||
return _filters.at(m);
|
||||
}
|
||||
|
||||
std::size_t MelFilterBank::getFilterCount() const
|
||||
{
|
||||
return _filters.size();
|
||||
}
|
||||
|
||||
std::size_t MelFilterBank::getBinCount() const
|
||||
{
|
||||
return _binCount;
|
||||
}
|
||||
|
||||
float MelFilterBank::computeEnergy(std::size_t m, std::span<const float> input) const
|
||||
{
|
||||
if (input.size() != _binCount)
|
||||
throw Exception{ "Input size must be equal to the number of bins" };
|
||||
|
||||
const auto& filter{ _filters.at(m) };
|
||||
assert(filter.leftBinIndex + filter.weights.size() <= input.size());
|
||||
|
||||
float energy{};
|
||||
std::size_t bin{ filter.leftBinIndex };
|
||||
for (std::size_t i{}, n = filter.weights.size(); i < n; ++i, ++bin)
|
||||
energy += input[bin] * filter.weights[i];
|
||||
|
||||
return energy;
|
||||
}
|
||||
|
||||
MelFilterBank computeMelFilterBank(std::size_t nfft, std::size_t sampleRate, std::size_t filterCount, float fMin, float fMax)
|
||||
{
|
||||
const float nyquist{ sampleRate / 2.F };
|
||||
const float effectiveFMin{ (fMin <= 0.F) ? 0.F : fMin };
|
||||
const float effectiveFMax{ (fMax <= 0.F || fMax > nyquist) ? nyquist : fMax };
|
||||
const float melMin{ freqToMel(effectiveFMin) };
|
||||
const float melMax{ freqToMel(effectiveFMax) };
|
||||
|
||||
// 1. mel points
|
||||
std::vector<float> melPoints(filterCount + 2);
|
||||
for (std::size_t i{}; i < melPoints.size(); ++i)
|
||||
melPoints[i] = melMin + i * (melMax - melMin) / (filterCount + 1);
|
||||
|
||||
// 2. mel -> Hz
|
||||
std::vector<float> freqs(filterCount + 2);
|
||||
std::transform(melPoints.begin(), melPoints.end(), freqs.begin(), melToFreq);
|
||||
|
||||
// 3. Hz -> bins
|
||||
std::vector<size_t> bins(filterCount + 2);
|
||||
for (std::size_t i{}; i < bins.size(); ++i)
|
||||
bins[i] = static_cast<size_t>(std::floor(nfft * freqs[i] / sampleRate));
|
||||
|
||||
// 4. fix duplicates
|
||||
for (std::size_t i{ 1 }; i < bins.size(); ++i)
|
||||
{
|
||||
if (bins[i] <= bins[i - 1])
|
||||
bins[i] = bins[i - 1] + 1;
|
||||
}
|
||||
|
||||
// 5. build filters
|
||||
const std::size_t binCount{ nfft / 2 + 1 };
|
||||
std::vector<MelFilterBank::Filter> filters{ filterCount };
|
||||
|
||||
for (std::size_t m{}; m < filterCount; ++m)
|
||||
{
|
||||
const std::size_t left{ bins[m] };
|
||||
const std::size_t center{ bins[m + 1] };
|
||||
const std::size_t right{ bins[m + 2] };
|
||||
|
||||
std::vector<float> filterWeights;
|
||||
filterWeights.reserve(right - left);
|
||||
|
||||
// rising edge of the triangle
|
||||
for (std::size_t k{ left }; k < center; ++k)
|
||||
filterWeights.push_back(float(k - left) / (center - left));
|
||||
|
||||
// falling edge of the triangle
|
||||
for (std::size_t k{ center }; k < right; ++k)
|
||||
filterWeights.push_back(float(right - k) / (right - center));
|
||||
|
||||
assert(filterWeights.size() == right - left);
|
||||
|
||||
// normalize the filter to sum = 1
|
||||
const float sum{ std::accumulate(filterWeights.begin(), filterWeights.end(), 0.F) };
|
||||
if (sum > 0.F)
|
||||
{
|
||||
for (float& weight : filterWeights)
|
||||
weight /= sum;
|
||||
}
|
||||
|
||||
filters[m] = MelFilterBank::Filter{ std::move(filterWeights), left };
|
||||
}
|
||||
|
||||
return MelFilterBank{ std::move(filters), binCount };
|
||||
}
|
||||
} // namespace lms::audio::features
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace lms::audio::features
|
||||
{
|
||||
float freqToMel(float freq);
|
||||
float melToFreq(float mel);
|
||||
|
||||
struct MelFilterBank
|
||||
{
|
||||
struct Filter
|
||||
{
|
||||
std::vector<float> weights; // normalized so that the sum of weights equals 1.0
|
||||
std::size_t leftBinIndex; // index of the leftmost FFT bin covered by the filter
|
||||
};
|
||||
|
||||
// binCount is the number of FFT bins (nfft/2 + 1) that the filters can cover
|
||||
MelFilterBank(std::vector<Filter>&& _filters, std::size_t binCount);
|
||||
|
||||
const Filter& getFilter(std::size_t m) const;
|
||||
std::size_t getFilterCount() const;
|
||||
std::size_t getBinCount() const;
|
||||
|
||||
float computeEnergy(std::size_t m, std::span<const float> input) const;
|
||||
|
||||
private:
|
||||
const std::vector<Filter> _filters;
|
||||
const std::size_t _binCount;
|
||||
};
|
||||
|
||||
// Each filter covers a range of FFT bins and is normalized so that the sum of its weights equals 1.0
|
||||
// Each filter stores only its non-zero triangular region (sparse representation)
|
||||
// fMin/fMax: frequency range in Hz. Defaults (0.f, 0.f) span from 0 to Nyquist.
|
||||
MelFilterBank computeMelFilterBank(size_t nfft, size_t sampleRate, size_t filterCount, float fMin = 0.F, float fMax = 0.F);
|
||||
} // namespace lms::audio::features
|
||||
@@ -285,7 +285,7 @@ namespace lms::audio::ffmpeg
|
||||
|
||||
bool AudioFile::hasAttachedPictures() const
|
||||
{
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
for (std::size_t i{}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
return true;
|
||||
@@ -304,7 +304,7 @@ namespace lms::audio::ffmpeg
|
||||
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
for (std::size_t i{}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
AVStream* avstream = _context->streams[i];
|
||||
|
||||
|
||||
@@ -127,6 +127,14 @@ namespace lms::audio::ffmpeg
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
_estimatedDuration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
|
||||
if (_estimatedDuration > offset)
|
||||
_estimatedDuration = _estimatedDuration - std::chrono::duration_cast<std::chrono::milliseconds>(offset);
|
||||
else
|
||||
_estimatedDuration = {};
|
||||
}
|
||||
|
||||
_decoderContext = AVCodecContextPtr{ ::avcodec_alloc_context3(decoder) };
|
||||
if (!_decoderContext)
|
||||
throw Exception{ "Cannot allocate decoder context" };
|
||||
@@ -225,7 +233,7 @@ namespace lms::audio::ffmpeg
|
||||
else
|
||||
{
|
||||
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{};
|
||||
for (size_t i = 0; i < outputChannelBuffers.size(); ++i)
|
||||
for (std::size_t i{}; i < outputChannelBuffers.size(); ++i)
|
||||
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data());
|
||||
|
||||
// Resample decoded audio
|
||||
@@ -266,6 +274,11 @@ namespace lms::audio::ffmpeg
|
||||
return _finished;
|
||||
}
|
||||
|
||||
std::chrono::milliseconds PcmDecoder::getEstimatedDuration() const
|
||||
{
|
||||
return _estimatedDuration;
|
||||
}
|
||||
|
||||
std::size_t PcmDecoder::computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const
|
||||
{
|
||||
if (_parameters.planar)
|
||||
@@ -343,7 +356,7 @@ namespace lms::audio::ffmpeg
|
||||
std::size_t PcmDecoder::drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel)
|
||||
{
|
||||
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{};
|
||||
for (size_t i = 0; i < outputChannelBuffers.size(); ++i)
|
||||
for (std::size_t i{}; i < outputChannelBuffers.size(); ++i)
|
||||
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data());
|
||||
|
||||
const int outSampleCount{ ::swr_convert(_resampleContext.get(),
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace lms::audio::ffmpeg
|
||||
std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) override;
|
||||
bool finished() const override;
|
||||
|
||||
std::chrono::milliseconds getEstimatedDuration() const override;
|
||||
|
||||
std::size_t computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const;
|
||||
void feedDecoder();
|
||||
std::size_t drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel);
|
||||
@@ -52,6 +54,7 @@ namespace lms::audio::ffmpeg
|
||||
bool _draining{};
|
||||
|
||||
AVFormatContextPtr _context;
|
||||
std::chrono::milliseconds _estimatedDuration{};
|
||||
int _inputStreamIndex{};
|
||||
AVCodecContextPtr _decoderContext;
|
||||
AVFramePtr _decodedFrame;
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace lms::audio::ffmpeg::utils
|
||||
|
||||
void avLogCallback(void*, int level, const char* fmt, va_list vl)
|
||||
{
|
||||
if (!core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
if (!core::Service<core::logging::ILogger>::get() || core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
return;
|
||||
|
||||
if (level > AV_LOG_WARNING)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MusicNNEmbeddingExtractor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
#include "audio/IMusicNNEmbeddingExtractor.hpp"
|
||||
#include "math/StatsAccumulator.hpp"
|
||||
#include "musicnn/MusicNNModel.hpp"
|
||||
|
||||
namespace lms::audio::musicnn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr float minMeaningfulPatchRms{ 0.003F };
|
||||
|
||||
template<typename FloatType>
|
||||
FloatType computeRms(std::span<const FloatType> samples)
|
||||
{
|
||||
const FloatType sumSq{ std::transform_reduce(samples.begin(), samples.end(), FloatType{}, std::plus<>{}, [](FloatType s) { return s * s; }) };
|
||||
return std::sqrt(sumSq / static_cast<FloatType>(samples.size()));
|
||||
}
|
||||
|
||||
// We want something like this:
|
||||
// gap patch(0) gap patch(1) gap patch(maxPatchCount) gap
|
||||
std::size_t computePatchGap(std::size_t totalFrameCount, std::size_t patchFrameCount, std::size_t maxPatchCount)
|
||||
{
|
||||
assert(maxPatchCount > 0);
|
||||
assert(patchFrameCount > 0);
|
||||
|
||||
const std::size_t patchCount{ std::min(maxPatchCount, totalFrameCount / patchFrameCount) };
|
||||
if (patchCount == 0)
|
||||
return 0;
|
||||
|
||||
return (totalFrameCount - patchCount * patchFrameCount) / (patchCount + 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Accumulates one 187-frame MusicNN mel patch
|
||||
class MusicNNEmbeddingExtractor::PatchAccumulator
|
||||
{
|
||||
public:
|
||||
void addMelRow(std::span<const float, melBandCount> melRow, float frameRms)
|
||||
{
|
||||
assert(_frameCount < patchFrameCount);
|
||||
const std::size_t offset{ _frameCount * melBandCount };
|
||||
std::copy(melRow.begin(), melRow.end(), _melMatrix.begin() + static_cast<std::ptrdiff_t>(offset));
|
||||
_rmsAccum += frameRms;
|
||||
++_frameCount;
|
||||
}
|
||||
|
||||
void reset() noexcept
|
||||
{
|
||||
_frameCount = {};
|
||||
_rmsAccum = {};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool complete() const { return _frameCount == patchFrameCount; }
|
||||
|
||||
[[nodiscard]] bool meaningful() const
|
||||
{
|
||||
return (_frameCount > 0) && ((_rmsAccum / static_cast<float>(_frameCount)) >= minMeaningfulPatchRms);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const float, patchFrameCount * melBandCount> data() const
|
||||
{
|
||||
return _melMatrix;
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t _frameCount{};
|
||||
float _rmsAccum{};
|
||||
std::array<float, patchFrameCount * melBandCount> _melMatrix;
|
||||
};
|
||||
|
||||
MusicNNEmbeddingExtractor::MusicNNEmbeddingExtractor(const std::filesystem::path& modelPath, std::size_t maxPatchCount)
|
||||
: _melFilterBank{ features::computeMelFilterBank(fftSize, sampleRate, melBandCount, melFMin, melFMax) }
|
||||
, _model{ modelPath }
|
||||
, _maxPatchCount{ maxPatchCount }
|
||||
{
|
||||
static_assert(MusicNNEmbeddingExtractor::windowSize == MusicNNEmbeddingExtractor::fftSize);
|
||||
if (_maxPatchCount <= 0)
|
||||
throw audio::Exception{ "MusicNN embedding extractor: max patch count must be > 0" };
|
||||
}
|
||||
|
||||
IMusicNNEmbeddingExtractor::ExtractionResult MusicNNEmbeddingExtractor::extract(const std::filesystem::path& audioFile) const
|
||||
{
|
||||
auto frameDecoder{ std::make_unique<FrameDecoder>(audioFile,
|
||||
PcmParameters{ .channelCount = 1,
|
||||
.sampleRate = static_cast<unsigned>(sampleRate),
|
||||
.sampleType = PcmSampleType::Float32,
|
||||
.byteOrder = std::endian::native,
|
||||
.planar = false },
|
||||
frameHopSamples) };
|
||||
|
||||
std::array<float, melBandCount> logMelRow{};
|
||||
std::array<math::StatsAccumulator<float>, decltype(_model)::outputSize> embeddingAccumulators;
|
||||
ExtractionResult result;
|
||||
const std::size_t estimatedFrameCount{ frameDecoder->getEstimatedFrameCount() };
|
||||
|
||||
// Fallback: use a gap of two patch lengths if the frame count is unknown
|
||||
const std::size_t patchGapFrameCount{ estimatedFrameCount ? computePatchGap(frameDecoder->getEstimatedFrameCount(), patchFrameCount, _maxPatchCount) : (2 * patchFrameCount) };
|
||||
const auto patchAccumulator{ std::make_unique<PatchAccumulator>() };
|
||||
|
||||
while (true)
|
||||
{
|
||||
patchAccumulator->reset();
|
||||
|
||||
const auto onFrame{ [&](const FrameDecoder::SpectralFrameView& frame) {
|
||||
// MusicNN log compression: log10(10000 * mel + 1)
|
||||
for (std::size_t m{}; m < melBandCount; ++m)
|
||||
{
|
||||
const float energy{ _melFilterBank.computeEnergy(m, std::span<const float>(frame.powerSpectrum)) };
|
||||
logMelRow[m] = std::log10(10000.F * energy + 1.F);
|
||||
}
|
||||
|
||||
const float rms{ computeRms(frame.rawSamples.subspan(0, frameHopSamples)) };
|
||||
patchAccumulator->addMelRow(logMelRow, rms);
|
||||
} };
|
||||
|
||||
if (patchGapFrameCount > 0 && frameDecoder->skipFrames(patchGapFrameCount) == 0)
|
||||
break;
|
||||
|
||||
if (frameDecoder->decodeFrames(patchFrameCount, onFrame) < patchFrameCount)
|
||||
break;
|
||||
|
||||
assert(patchAccumulator->complete());
|
||||
|
||||
if (!patchAccumulator->meaningful())
|
||||
continue;
|
||||
|
||||
const auto embedding{ _model.forward(patchAccumulator->data()) };
|
||||
for (std::size_t d{}; d < embedding.size(); ++d)
|
||||
embeddingAccumulators[d].add(embedding[d]);
|
||||
++result.patchCount;
|
||||
}
|
||||
|
||||
if (result.patchCount > 0)
|
||||
{
|
||||
for (std::size_t d{}; d < decltype(_model)::outputSize; ++d)
|
||||
result.embeddings.mean.values[d] = embeddingAccumulators[d].getMean();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace lms::audio::musicnn
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/IMusicNNEmbeddingExtractor.hpp"
|
||||
|
||||
#include "MusicNNModel.hpp"
|
||||
#include "features/MelFilterBank.hpp"
|
||||
#include "utils/PcmSpectralFrameDecoder.hpp"
|
||||
|
||||
namespace lms::audio::musicnn
|
||||
{
|
||||
class MusicNNEmbeddingExtractor : public IMusicNNEmbeddingExtractor
|
||||
{
|
||||
public:
|
||||
MusicNNEmbeddingExtractor(const std::filesystem::path& modelPath, std::size_t maxPatchCount);
|
||||
~MusicNNEmbeddingExtractor() override = default;
|
||||
|
||||
MusicNNEmbeddingExtractor(const MusicNNEmbeddingExtractor&) = delete;
|
||||
MusicNNEmbeddingExtractor& operator=(const MusicNNEmbeddingExtractor&) = delete;
|
||||
|
||||
private:
|
||||
[[nodiscard]] ExtractionResult extract(const std::filesystem::path& audioFile) const override;
|
||||
|
||||
// MusicNN signal processing constants (from musicnn/configuration.py and musicnn_torch.py)
|
||||
static constexpr std::size_t sampleRate{ 16'000 };
|
||||
static constexpr std::size_t windowSize{ 512 }; // 512-sample Hann window (32 ms)
|
||||
static constexpr std::size_t fftSize{ 512 };
|
||||
static constexpr std::size_t frameHopSamples{ 256 }; // 16 ms hop (matches FFT_HOP in musicnn)
|
||||
static constexpr std::size_t melBandCount{ 96 };
|
||||
static constexpr float melFMin{ 0.F };
|
||||
static constexpr float melFMax{ 8'000.F };
|
||||
static constexpr std::size_t patchFrameCount{ MusicNNModel::inputFrames }; // 187 frames = 3 s
|
||||
|
||||
class PatchAccumulator;
|
||||
|
||||
using FrameDecoder = PcmSpectralFrameDecoder<512, float>;
|
||||
const features::MelFilterBank _melFilterBank;
|
||||
const MusicNNModel _model;
|
||||
const std::size_t _maxPatchCount;
|
||||
};
|
||||
} // namespace lms::audio::musicnn
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/MusicNNEmbeddings.hpp"
|
||||
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
static_assert(sizeof(float) == 4);
|
||||
static_assert(std::numeric_limits<float>::is_iec559);
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t byteswap32(uint32_t x)
|
||||
{
|
||||
return (x >> 24) | ((x >> 8) & 0x0000FF00u) | ((x << 8) & 0x00FF0000u) | (x << 24);
|
||||
}
|
||||
|
||||
void writeFloats(std::span<const float> data, std::span<std::byte> blob)
|
||||
{
|
||||
if (blob.size() < data.size() * sizeof(uint32_t))
|
||||
throw Exception{ "Buffer too small to write MusicNN embeddings" };
|
||||
|
||||
for (std::size_t i{}; i < data.size(); ++i)
|
||||
{
|
||||
uint32_t bits{ std::bit_cast<uint32_t>(data[i]) };
|
||||
if constexpr (std::endian::native == std::endian::little)
|
||||
bits = byteswap32(bits);
|
||||
std::memcpy(blob.data() + i * 4, &bits, 4);
|
||||
}
|
||||
}
|
||||
|
||||
void readFloats(std::span<const std::byte> blob, std::span<float> data)
|
||||
{
|
||||
if (blob.size() < data.size() * sizeof(uint32_t))
|
||||
throw Exception{ "Buffer too small to read MusicNN embeddings" };
|
||||
|
||||
for (std::size_t i{}; i < data.size(); ++i)
|
||||
{
|
||||
uint32_t bits{};
|
||||
std::memcpy(&bits, blob.data() + i * 4, 4);
|
||||
if constexpr (std::endian::native == std::endian::little)
|
||||
bits = byteswap32(bits);
|
||||
data[i] = std::bit_cast<float>(bits);
|
||||
}
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
void trackMusicNNEmbeddingsToBlob(const TrackMusicNNEmbeddings& embeddings, std::span<std::byte> buffer)
|
||||
{
|
||||
if (buffer.size() < sizeof(TrackMusicNNEmbeddings))
|
||||
throw Exception{ "Buffer too small to write TrackMusicNNEmbeddings" };
|
||||
|
||||
writeFloats(embeddings.mean.values, buffer.subspan(0, MusicNNEmbedding::size * sizeof(float)));
|
||||
}
|
||||
|
||||
void trackMusicNNEmbeddingsFromBlob(std::span<const std::byte> buffer, TrackMusicNNEmbeddings& embeddings)
|
||||
{
|
||||
if (buffer.size() < sizeof(TrackMusicNNEmbeddings))
|
||||
throw Exception{ "Buffer too small to read TrackMusicNNEmbeddings" };
|
||||
|
||||
readFloats(buffer.subspan(0, MusicNNEmbedding::size * sizeof(float)), embeddings.mean.values);
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MusicNNModel.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <onnxruntime_cxx_api.h>
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
|
||||
namespace lms::audio::musicnn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Shape of the ONNX model's single input tensor: [batch=1, T=187, mel=96]
|
||||
const std::array<std::int64_t, 3> inputShape{ 1,
|
||||
static_cast<std::int64_t>(MusicNNModel::inputFrames),
|
||||
static_cast<std::int64_t>(MusicNNModel::inputBands) };
|
||||
|
||||
// Shape of the ONNX model's single output tensor: [batch=1, embedding=200]
|
||||
const std::array<std::int64_t, 2> outputShape{ 1,
|
||||
static_cast<std::int64_t>(MusicNNModel::outputSize) };
|
||||
|
||||
constexpr const char* inputName{ "mel_patch" };
|
||||
constexpr const char* outputName{ "embedding" };
|
||||
} // namespace
|
||||
|
||||
struct MusicNNModel::Impl
|
||||
{
|
||||
Ort::Env env;
|
||||
Ort::SessionOptions sessionOptions;
|
||||
Ort::Session session;
|
||||
Ort::MemoryInfo memoryInfo;
|
||||
|
||||
explicit Impl(const std::filesystem::path& onnxPath)
|
||||
: env{ ORT_LOGGING_LEVEL_ERROR, "MusicNN" }
|
||||
, session{ [&]() -> Ort::Session {
|
||||
sessionOptions.SetIntraOpNumThreads(1);
|
||||
sessionOptions.SetInterOpNumThreads(1);
|
||||
return Ort::Session{ env, onnxPath.c_str(), sessionOptions };
|
||||
}() }
|
||||
, memoryInfo{ Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault) }
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
MusicNNModel::MusicNNModel(const std::filesystem::path& onnxPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
_impl = std::make_unique<Impl>(onnxPath);
|
||||
}
|
||||
catch (const Ort::Exception& e)
|
||||
{
|
||||
throw audio::Exception{ std::string{ "Failed to load ONNX model '" } + onnxPath.string() + "': " + e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
MusicNNModel::~MusicNNModel() = default;
|
||||
|
||||
std::array<float, MusicNNModel::outputSize> MusicNNModel::forward(
|
||||
std::span<const float, inputFrames * inputBands> melPatch) const
|
||||
{
|
||||
Ort::Value inputTensor{ Ort::Value::CreateTensor<float>(
|
||||
_impl->memoryInfo,
|
||||
const_cast<float*>(melPatch.data()), // safe cast
|
||||
melPatch.size(),
|
||||
inputShape.data(),
|
||||
inputShape.size()) };
|
||||
|
||||
std::array<float, outputSize> result{};
|
||||
Ort::Value outputTensor{ Ort::Value::CreateTensor<float>(
|
||||
_impl->memoryInfo,
|
||||
result.data(),
|
||||
result.size(),
|
||||
outputShape.data(),
|
||||
outputShape.size()) };
|
||||
|
||||
try
|
||||
{
|
||||
auto inputNames{ std::to_array({ inputName }) };
|
||||
auto outputNames{ std::to_array({ outputName }) };
|
||||
_impl->session.Run(Ort::RunOptions{ nullptr },
|
||||
inputNames.data(), &inputTensor, 1,
|
||||
outputNames.data(), &outputTensor, 1);
|
||||
}
|
||||
catch (const Ort::Exception& e)
|
||||
{
|
||||
throw audio::Exception{ std::string{ "ONNX inference failed: " } + e.what() };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace lms::audio::musicnn
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
namespace lms::audio::musicnn
|
||||
{
|
||||
// The ONNX model must be exported with:
|
||||
// input "mel_patch" shape [1, 1, 187, 96]
|
||||
// output "embedding" shape [1, 200]
|
||||
//
|
||||
// Export script: tools/musicnn/export_onnx.py
|
||||
class MusicNNModel
|
||||
{
|
||||
public:
|
||||
explicit MusicNNModel(const std::filesystem::path& onnxPath);
|
||||
~MusicNNModel();
|
||||
|
||||
MusicNNModel(const MusicNNModel&) = delete;
|
||||
MusicNNModel& operator=(const MusicNNModel&) = delete;
|
||||
|
||||
static inline constexpr std::size_t inputFrames{ 187 };
|
||||
static inline constexpr std::size_t inputBands{ 96 };
|
||||
static inline constexpr std::size_t outputSize{ 200 };
|
||||
|
||||
[[nodiscard]] std::array<float, outputSize> forward(std::span<const float, inputFrames * inputBands> melPatch) const;
|
||||
|
||||
private:
|
||||
// Pimpl: keep ORT headers out of translation units that include this header.
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> _impl;
|
||||
};
|
||||
} // namespace lms::audio::musicnn
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS 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.
|
||||
*
|
||||
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cassert>
|
||||
#include <complex>
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "core/AlignedHeapArray.hpp"
|
||||
|
||||
#include "audio/IPcmDecoder.hpp"
|
||||
#include "audio/PcmTypes.hpp"
|
||||
#include "math/FFT.hpp"
|
||||
#include "math/Window.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
// Stateful PCM frame decoder that applies a Hann window + FFT per frame.
|
||||
template<std::size_t WindowSize, typename FloatType = float>
|
||||
class PcmSpectralFrameDecoder
|
||||
{
|
||||
static_assert(std::has_single_bit(WindowSize), "WindowSize must be a power of two");
|
||||
|
||||
public:
|
||||
using FFTPlan = math::FixedRealFFTPlan<WindowSize, FloatType>;
|
||||
static constexpr std::size_t spectrumSize{ FFTPlan::getOutputSize() };
|
||||
|
||||
PcmSpectralFrameDecoder(const std::filesystem::path& audioFile, const PcmParameters& params, std::size_t hopSize)
|
||||
: PcmSpectralFrameDecoder{ createPcmDecoder(audioFile, {}, params), hopSize }
|
||||
{
|
||||
}
|
||||
~PcmSpectralFrameDecoder() = default;
|
||||
|
||||
explicit PcmSpectralFrameDecoder(std::unique_ptr<IPcmDecoder> decoder, std::size_t hopSize)
|
||||
: _pcmParams{ decoder->getParameters() }
|
||||
, _hopSize{ hopSize }
|
||||
, _powerScale{ FloatType{ 1 } / (_window.energy() * static_cast<FloatType>(WindowSize)) }
|
||||
, _decoder{ std::move(decoder) }
|
||||
, _samplesBuffer(bufferFrameCount * _hopSize + WindowSize)
|
||||
, _bufferedSampleCount{ WindowSize / 2 } // first analysis frame centered on sample 0, matching librosa center=True semantics.
|
||||
{
|
||||
assert(_hopSize > 0);
|
||||
}
|
||||
|
||||
PcmSpectralFrameDecoder(const PcmSpectralFrameDecoder&) = delete;
|
||||
PcmSpectralFrameDecoder& operator=(const PcmSpectralFrameDecoder&) = delete;
|
||||
|
||||
std::size_t hopSize() const noexcept { return _hopSize; }
|
||||
const PcmParameters& pcmParameters() const noexcept { return _pcmParams; }
|
||||
|
||||
std::size_t getEstimatedFrameCount() const
|
||||
{
|
||||
const auto duration{ _decoder->getEstimatedDuration() };
|
||||
if (duration <= std::chrono::milliseconds::zero())
|
||||
return 0;
|
||||
|
||||
const auto totalSamples{ static_cast<std::size_t>((static_cast<std::uint64_t>(duration.count()) * _pcmParams.sampleRate) / 1'000) };
|
||||
|
||||
constexpr std::size_t halfWindow{ WindowSize / 2 };
|
||||
if (totalSamples < halfWindow) // Not enough samples to produce even the first frame.
|
||||
return 0;
|
||||
|
||||
return 1 + ((totalSamples - halfWindow) / _hopSize);
|
||||
}
|
||||
|
||||
// Spectral data for a single frame.
|
||||
struct SpectralFrameView
|
||||
{
|
||||
std::span<const FloatType, WindowSize> rawSamples;
|
||||
std::span<const FloatType, spectrumSize> powerSpectrum;
|
||||
};
|
||||
|
||||
// Decodes up to frameCount frames, invoking callback for each. May return fewer than
|
||||
// frameCount at EOF. Returns 0 only if no frame at all could be decoded.
|
||||
template<typename Callback>
|
||||
requires std::invocable<Callback, const SpectralFrameView&>
|
||||
std::size_t decodeFrames(std::size_t frameCount, Callback&& callback)
|
||||
{
|
||||
if (frameCount == 0)
|
||||
return 0;
|
||||
|
||||
std::size_t decodedCount{};
|
||||
while (decodedCount < frameCount)
|
||||
{
|
||||
if (!readAtLeastSamples(std::max(WindowSize, _hopSize)))
|
||||
break;
|
||||
|
||||
const std::span<const FloatType, WindowSize> rawSamples{ _samplesBuffer.data(), WindowSize };
|
||||
const std::span<FloatType, WindowSize> windowedFrame{ _windowedFrame.data(), WindowSize };
|
||||
_window.apply(rawSamples, windowedFrame);
|
||||
|
||||
_fftPlan.apply(_windowedFrame, _fftOutput);
|
||||
|
||||
// Reuse _windowedFrame for power spectrum (spectrumSize <= WindowSize).
|
||||
const std::span<FloatType, spectrumSize> powerBuffer{ _windowedFrame.data(), spectrumSize };
|
||||
std::transform(_fftOutput.cbegin(), _fftOutput.cend(), powerBuffer.begin(),
|
||||
[this](const std::complex<FloatType>& bin) {
|
||||
return (bin.real() * bin.real() + bin.imag() * bin.imag()) * _powerScale;
|
||||
});
|
||||
|
||||
const SpectralFrameView frame{ .rawSamples = rawSamples,
|
||||
.powerSpectrum = std::span<const FloatType, spectrumSize>{ _windowedFrame.data(), spectrumSize } };
|
||||
std::invoke(callback, frame);
|
||||
|
||||
consumeSamples(_hopSize);
|
||||
++_currentFrameIndex;
|
||||
++decodedCount;
|
||||
}
|
||||
|
||||
return decodedCount;
|
||||
}
|
||||
|
||||
// Skips exactly the next frameCount frames without computing FFT or invoking callbacks.
|
||||
// Returns frameCount on success, 0 on EOF.
|
||||
std::size_t skipFrames(std::size_t frameCount)
|
||||
{
|
||||
if (frameCount == 0)
|
||||
return 0;
|
||||
|
||||
std::size_t skippedFrameCount{};
|
||||
while (skippedFrameCount < frameCount)
|
||||
{
|
||||
if (!readAtLeastSamples(std::max(WindowSize, _hopSize)))
|
||||
break;
|
||||
|
||||
consumeSamples(_hopSize);
|
||||
++_currentFrameIndex;
|
||||
++skippedFrameCount;
|
||||
}
|
||||
|
||||
return skippedFrameCount;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t currentFrameIndex() const noexcept { return _currentFrameIndex; }
|
||||
|
||||
private:
|
||||
static constexpr std::size_t bufferFrameCount{ 20 };
|
||||
|
||||
bool readAtLeastSamples(std::size_t sampleCount)
|
||||
{
|
||||
if (sampleCount <= _bufferedSampleCount)
|
||||
return true;
|
||||
|
||||
if (_samplesBuffer.size() < sampleCount)
|
||||
_samplesBuffer.resize(sampleCount);
|
||||
|
||||
while ((_bufferedSampleCount < sampleCount) && !_endOfStream)
|
||||
{
|
||||
std::span<FloatType> dest{ _samplesBuffer.data() + _bufferedSampleCount, _samplesBuffer.size() - _bufferedSampleCount };
|
||||
assert(!dest.empty());
|
||||
if (dest.empty())
|
||||
break;
|
||||
|
||||
std::array outputBuffers{ IPcmDecoder::WritableBuffer{ std::as_writable_bytes(dest) } };
|
||||
const std::size_t samplesRead{ _decoder->readSamples(outputBuffers) };
|
||||
if (samplesRead == 0)
|
||||
{
|
||||
_endOfStream = true;
|
||||
break;
|
||||
}
|
||||
|
||||
_bufferedSampleCount += samplesRead;
|
||||
}
|
||||
|
||||
return _bufferedSampleCount >= sampleCount;
|
||||
}
|
||||
|
||||
void consumeSamples(std::size_t samplesToDrop)
|
||||
{
|
||||
// TODO use a circular buffer and only compacts at the end of the buffer
|
||||
assert(samplesToDrop <= _bufferedSampleCount);
|
||||
|
||||
const auto remaining{ _bufferedSampleCount - samplesToDrop };
|
||||
if (remaining)
|
||||
{
|
||||
std::move(_samplesBuffer.begin() + samplesToDrop,
|
||||
_samplesBuffer.begin() + _bufferedSampleCount,
|
||||
_samplesBuffer.begin());
|
||||
}
|
||||
|
||||
_bufferedSampleCount = remaining;
|
||||
}
|
||||
|
||||
const PcmParameters _pcmParams;
|
||||
const std::size_t _hopSize;
|
||||
const math::HannWindow<WindowSize, FloatType> _window;
|
||||
const FloatType _powerScale;
|
||||
const FFTPlan _fftPlan{};
|
||||
std::unique_ptr<IPcmDecoder> _decoder;
|
||||
std::vector<FloatType> _samplesBuffer;
|
||||
core::AlignedHeapArray<FloatType, FFTPlan::minBufferAlignment> _windowedFrame{ FFTPlan::getInputSize() };
|
||||
core::AlignedHeapArray<std::complex<FloatType>, FFTPlan::minBufferAlignment> _fftOutput{ FFTPlan::getOutputSize() };
|
||||
std::size_t _bufferedSampleCount{};
|
||||
std::size_t _currentFrameIndex{};
|
||||
bool _endOfStream{};
|
||||
};
|
||||
} // namespace lms::audio
|
||||
Reference in New Issue
Block a user