Restored recommendations based on acoustic similarities (using musicnn), fixes #301
This commit is contained in:
+22
-1
@@ -1,5 +1,26 @@
|
|||||||
---
|
---
|
||||||
Checks: '-*,bugprone-*,concurrency-*,cppcoreguidelines-*,misc-*,-misc-include-cleaner,-misc-no-recursion,performance*,portability-*,readability-*,-readability-braces-around-statements,-readability-identifier-length,-readability-implicit-bool-conversion'
|
Checks: >
|
||||||
|
-*,
|
||||||
|
bugprone-*,
|
||||||
|
concurrency-*,
|
||||||
|
|
||||||
|
cppcoreguidelines-*,
|
||||||
|
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||||
|
-cppcoreguidelines-avoid-magic-numbers,
|
||||||
|
|
||||||
|
misc-*,
|
||||||
|
-misc-include-cleaner,
|
||||||
|
-misc-no-recursion,
|
||||||
|
|
||||||
|
performance*,
|
||||||
|
portability-*,
|
||||||
|
|
||||||
|
readability-*,
|
||||||
|
-readability-braces-around-statements,
|
||||||
|
-readability-identifier-length,
|
||||||
|
-readability-implicit-bool-conversion,
|
||||||
|
-readability-magic-numbers,
|
||||||
|
|
||||||
WarningsAsErrors: ''
|
WarningsAsErrors: ''
|
||||||
HeaderFilterRegex: ''
|
HeaderFilterRegex: ''
|
||||||
FormatStyle: none
|
FormatStyle: none
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y cmake pkgconf boost-libs ffmpeg stb libconfig taglib libarchive xxhash wt googletest pugixml pulseaudio
|
pkg install -y cmake pkgconf boost-libs ffmpeg stb libconfig taglib libarchive xxhash wt googletest onnxruntime pugixml pulseaudio
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
|
LMSROOT=$(pwd)
|
||||||
mkdir build
|
mkdir build
|
||||||
cd build
|
cd build
|
||||||
cmake .. -DCMAKE_UNITY_BUILD=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DBUILD_BENCHMARKS=OFF
|
cmake .. -DCMAKE_UNITY_BUILD=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DBUILD_BENCHMARKS=OFF
|
||||||
make -j$(nproc)
|
make -j$(nproc)
|
||||||
make test
|
LMS_MUSICNN_MODEL=$LMSROOT/src/libs/audio/models/MSD_musicnn_embedding.onnx make test
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
brew install pkg-config cmake boost ffmpeg libconfig taglib libarchive xxhash pugixml googletest openssl git
|
brew install pkg-config cmake boost ffmpeg libconfig taglib libarchive xxhash pugixml googletest onnxruntime openssl git
|
||||||
|
|
||||||
- name: Build and install Wt
|
- name: Build and install Wt
|
||||||
run: |
|
run: |
|
||||||
@@ -48,4 +48,4 @@ jobs:
|
|||||||
run: cmake --build build --parallel 2
|
run: cmake --build build --parallel 2
|
||||||
|
|
||||||
- name: Test LMS
|
- name: Test LMS
|
||||||
run: ctest --test-dir build --output-on-failure
|
run: LMS_MUSICNN_MODEL=$GITHUB_WORKSPACE/src/libs/audio/models/MSD_musicnn_embedding.onnx ctest --test-dir build --output-on-failure
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
name: Install dependencies (cpp)
|
name: Install dependencies (cpp)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install --yes build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev libstb-dev libtag1-dev libpam0g-dev libgtest-dev libarchive-dev libxxhash-dev libpugixml-dev
|
sudo apt-get install --yes build-essential cmake libarchive-dev libavcodec-dev libavformat-dev libavutil-dev libboost-all-dev libconfig++-dev libgtest-dev libpam0g-dev libpugixml-dev libstb-dev libtag1-dev libxxhash-dev
|
||||||
export WT_VERSION=4.11.3
|
export WT_VERSION=4.11.3
|
||||||
export WT_INSTALL_PREFIX=/usr
|
export WT_INSTALL_PREFIX=/usr
|
||||||
git clone https://github.com/emweb/wt.git /tmp/wt
|
git clone https://github.com/emweb/wt.git /tmp/wt
|
||||||
|
|||||||
+1
-4
@@ -49,9 +49,7 @@ if (BUILD_BENCHMARKS)
|
|||||||
message(STATUS "Building benchmarks")
|
message(STATUS "Building benchmarks")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
add_compile_definitions($<$<NOT:$<CONFIG:Debug>>:NDEBUG>)
|
||||||
add_definitions(-DNDEBUG)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
@@ -60,4 +58,3 @@ install(DIRECTORY docroot DESTINATION share/lms)
|
|||||||
install(FILES conf/systemd/default.service DESTINATION share/lms)
|
install(FILES conf/systemd/default.service DESTINATION share/lms)
|
||||||
install(FILES conf/pam/lms DESTINATION share/lms)
|
install(FILES conf/pam/lms DESTINATION share/lms)
|
||||||
install(FILES conf/lms.conf DESTINATION share/lms)
|
install(FILES conf/lms.conf DESTINATION share/lms)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
|
||||||
FROM --platform=$BUILDPLATFORM alpine:3.21
|
FROM --platform=$BUILDPLATFORM alpine:3.23
|
||||||
|
|
||||||
ARG BUILD_PACKAGES="\
|
ARG BUILD_PACKAGES="\
|
||||||
clang \
|
clang \
|
||||||
@@ -22,6 +22,7 @@ ARG LMS_BUILD_PACKAGES=" \
|
|||||||
libarchive-dev \
|
libarchive-dev \
|
||||||
libconfig-dev \
|
libconfig-dev \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
|
onnxruntime-dev \
|
||||||
pugixml-dev \
|
pugixml-dev \
|
||||||
pulseaudio-dev \
|
pulseaudio-dev \
|
||||||
stb \
|
stb \
|
||||||
@@ -43,4 +44,4 @@ RUN \
|
|||||||
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=/$(xx-info)/usr/include -DCMAKE_UNITY_BUILD=ON -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} -DBUILD_BENCHMARKS=ON && \
|
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=/$(xx-info)/usr/include -DCMAKE_UNITY_BUILD=ON -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} -DBUILD_BENCHMARKS=ON && \
|
||||||
VERBOSE=1 make -j$(nproc) && \
|
VERBOSE=1 make -j$(nproc) && \
|
||||||
xx-verify src/lms/lms && \
|
xx-verify src/lms/lms && \
|
||||||
(xx-info is-cross || make test)
|
(xx-info is-cross || LMS_MUSICNN_MODEL=/tmp/lms/src/libs/audio/models/MSD_musicnn_embedding.onnx make test)
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ ARG BUILD_PACKAGES="\
|
|||||||
libconfig \
|
libconfig \
|
||||||
libpulse \
|
libpulse \
|
||||||
make \
|
make \
|
||||||
|
onnxruntime-cpu \
|
||||||
pkgconfig \
|
pkgconfig \
|
||||||
|
protobuf \
|
||||||
pugixml \
|
pugixml \
|
||||||
stb \
|
stb \
|
||||||
taglib \
|
taglib \
|
||||||
@@ -33,4 +35,4 @@ ARG LMS_UNITY_BUILD=ON
|
|||||||
ARG LMS_IMAGE_BACKEND=stb
|
ARG LMS_IMAGE_BACKEND=stb
|
||||||
RUN cmake .. -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} -DCMAKE_UNITY_BUILD=${LMS_UNITY_BUILD} -DLMS_IMAGE_BACKEND=${LMS_IMAGE_BACKEND} -DBUILD_BENCHMARKS=ON
|
RUN cmake .. -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} -DCMAKE_UNITY_BUILD=${LMS_UNITY_BUILD} -DLMS_IMAGE_BACKEND=${LMS_IMAGE_BACKEND} -DBUILD_BENCHMARKS=ON
|
||||||
RUN VERBOSE=1 make -j$(nproc)
|
RUN VERBOSE=1 make -j$(nproc)
|
||||||
RUN make test
|
RUN LMS_MUSICNN_MODEL=/tmp/lms/src/libs/audio/models/MSD_musicnn_embedding.onnx make test
|
||||||
|
|||||||
+10
-2
@@ -23,6 +23,7 @@ ARG BUILD_PACKAGES=" \
|
|||||||
libtool \
|
libtool \
|
||||||
libvorbis-dev \
|
libvorbis-dev \
|
||||||
make \
|
make \
|
||||||
|
onnxruntime-dev \
|
||||||
openjpeg-dev \
|
openjpeg-dev \
|
||||||
openssl-dev \
|
openssl-dev \
|
||||||
opus-dev \
|
opus-dev \
|
||||||
@@ -119,7 +120,7 @@ RUN \
|
|||||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||||
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_UNITY_BUILD=ON -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
|
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_UNITY_BUILD=ON -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
|
||||||
LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) install && \
|
LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) install && \
|
||||||
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
|
LD_LIBRARY_PATH=${PREFIX}/lib LMS_MUSICNN_MODEL=/tmp/lms/src/libs/audio/models/MSD_musicnn_embedding.onnx make test && \
|
||||||
mkdir -p ${PREFIX}/etc/ && \
|
mkdir -p ${PREFIX}/etc/ && \
|
||||||
cp /tmp/lms/conf/lms.conf ${PREFIX}/etc
|
cp /tmp/lms/conf/lms.conf ${PREFIX}/etc
|
||||||
|
|
||||||
@@ -162,6 +163,7 @@ ARG RUNTIME_PACKAGES=" \
|
|||||||
libpulse \
|
libpulse \
|
||||||
libvorbis \
|
libvorbis \
|
||||||
libssl3 \
|
libssl3 \
|
||||||
|
onnxruntime \
|
||||||
openjpeg \
|
openjpeg \
|
||||||
opus \
|
opus \
|
||||||
pugixml \
|
pugixml \
|
||||||
@@ -171,7 +173,13 @@ ARG RUNTIME_PACKAGES=" \
|
|||||||
ARG LMS_USER=lms
|
ARG LMS_USER=lms
|
||||||
ARG LMS_GROUP=lms
|
ARG LMS_GROUP=lms
|
||||||
|
|
||||||
RUN apk add --no-cache --update ${RUNTIME_PACKAGES}
|
# Install packages and remove useless stuff brought by onnxruntime
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache --update ${RUNTIME_PACKAGES} && \
|
||||||
|
rm -f \
|
||||||
|
/usr/bin/protoc* \
|
||||||
|
/usr/bin/onnx* \
|
||||||
|
/usr/lib/libprotoc*
|
||||||
|
|
||||||
RUN addgroup -S ${LMS_GROUP} && \
|
RUN addgroup -S ${LMS_GROUP} && \
|
||||||
adduser -S -H ${LMS_USER} && \
|
adduser -S -H ${LMS_USER} && \
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ apt-get install build-essential cmake libboost-program-options-dev libboost-syst
|
|||||||
__Optional dependencies__:
|
__Optional dependencies__:
|
||||||
* `libpam0g-dev`: used to handle PAM authentication
|
* `libpam0g-dev`: used to handle PAM authentication
|
||||||
* `libpulse-dev`: used to output audio via PulseAudio in jukebox mode
|
* `libpulse-dev`: used to output audio via PulseAudio in jukebox mode
|
||||||
|
* `libonnxruntime-dev`: used to extract MusicNN embeddings from tracks for the recommendation engine
|
||||||
|
|
||||||
__Notes__:
|
__Notes__:
|
||||||
* `libstb-dev` can be replaced by `libgraphicsmagick++1-dev` (the latter will likely use more RAM)
|
* `libstb-dev` can be replaced by `libgraphicsmagick++1-dev` (the latter will likely use more RAM)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<form>
|
<form>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
${export-query-plans-btn class="btn btn-primary"}
|
${export-query-profiling-btn class="btn btn-primary"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -90,12 +90,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label" for="${id:similarity-engine-type}">
|
<label class="form-label" for="${id:recommendation-engine-type}">
|
||||||
${tr:Lms.Admin.Database.similarity-engine-type}
|
${tr:Lms.Admin.Database.recommendation-engine-type}
|
||||||
</label>
|
</label>
|
||||||
${similarity-engine-type class="form-control"}
|
${recommendation-engine-type class="form-control"}
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
${similarity-engine-type-info}
|
${recommendation-engine-type-info}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+4
-4
@@ -37,10 +37,10 @@
|
|||||||
<h3>${tr:Lms.Explore.tracks}</h3>
|
<h3>${tr:Lms.Explore.tracks}</h3>
|
||||||
${tracks class="mb-3"}
|
${tracks class="mb-3"}
|
||||||
${</if-has-non-release-tracks>}
|
${</if-has-non-release-tracks>}
|
||||||
${<if-has-similar-artists>}
|
${<if-has-related-artists>}
|
||||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
<h3>${tr:Lms.Explore.Artist.related-artists}</h3>
|
||||||
${similar-artists class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
|
${related-artists class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
|
||||||
${</if-has-similar-artists>}
|
${</if-has-related-artists>}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Artist.template.release-container">
|
<message id="Lms.Explore.Artist.template.release-container">
|
||||||
|
|||||||
+12
-8
@@ -94,13 +94,15 @@
|
|||||||
<message id="Lms.Admin.Database.misc">Miscellaneous</message>
|
<message id="Lms.Admin.Database.misc">Miscellaneous</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
||||||
<message id="Lms.Admin.Database.never">Never</message>
|
<message id="Lms.Admin.Database.never">Never</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-not-supported">This engine is not supported</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type">Recommendation engine</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.audio-similarity">Audio similarity</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Tags</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.none">None</message>
|
||||||
<message id="Lms.Admin.Database.scan-aborted">Scan aborted!</message>
|
<message id="Lms.Admin.Database.scan-aborted">Scan aborted!</message>
|
||||||
<message id="Lms.Admin.Database.scan-complete">Scan complete: {1} total files, {2} additions, {3} updates, {4} deletions, {5} duplicates, {6} errors</message>
|
<message id="Lms.Admin.Database.scan-complete">Scan complete: {1} total files, {2} additions, {3} updates, {4} deletions, {5} duplicates, {6} errors</message>
|
||||||
<message id="Lms.Admin.Database.scan-launched">Scan launched!</message>
|
<message id="Lms.Admin.Database.scan-launched">Scan launched!</message>
|
||||||
<message id="Lms.Admin.Database.scan-settings">Scan settings</message>
|
<message id="Lms.Admin.Database.scan-settings">Scan settings</message>
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type">Similarity engine</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Tag-based</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.none">None</message>
|
|
||||||
<message id="Lms.Admin.Database.skip-single-release-playlists">Skip playlists that contain tracks from the same album</message>
|
<message id="Lms.Admin.Database.skip-single-release-playlists">Skip playlists that contain tracks from the same album</message>
|
||||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">The tag delimiter must not consist solely of spaces</message>
|
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">The tag delimiter must not consist solely of spaces</message>
|
||||||
<message id="Lms.Admin.Database.tag-parsing">Tag parsing</message>
|
<message id="Lms.Admin.Database.tag-parsing">Tag parsing</message>
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
|
||||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Unable to read the image at index {1}: {2}</message>
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">Unable to read the image at index {1}: {2}</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.cannot-extract-musicnn-embeddings">Cannot extract MusicNN embeddings: {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file ({1})</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file ({1})</message>
|
||||||
@@ -148,11 +151,12 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracting MusicNN embeddings: {1} of {2} files ({3}%)</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message>
|
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-reloading-recommendation-engine">Reloading recommendation engine: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Removing orphaned entries: {1} entries...</message>
|
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Removing orphaned entries: {1} entries...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Library analysis: {1} files...</message>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">Analyzing library: {1} files...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">Step status</message>
|
<message id="Lms.Admin.ScannerController.step-status">Step status</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Updating library fields: {1} entries</message>
|
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Updating library fields: {1} entries</message>
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@
|
|||||||
<message id="Lms.Admin.DebugTools.debug-tools">Debug tools</message>
|
<message id="Lms.Admin.DebugTools.debug-tools">Debug tools</message>
|
||||||
|
|
||||||
<!--Db-->
|
<!--Db-->
|
||||||
<message id="Lms.Admin.DebugTools.Db.export-query-plans">Export query plans</message>
|
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Export query profiling data</message>
|
||||||
<message id="Lms.Admin.DebugTools.Db.db">Database</message>
|
<message id="Lms.Admin.DebugTools.Db.db">Database</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
@@ -277,13 +281,13 @@
|
|||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.Artist.appears-on">Appears on</message>
|
<message id="Lms.Explore.Artist.appears-on">Appears on</message>
|
||||||
<message id="Lms.Explore.Artist.biography">Biography</message>
|
<message id="Lms.Explore.Artist.biography">Biography</message>
|
||||||
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
|
<message id="Lms.Explore.Artist.related-artists">Related artists</message>
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||||
<message id="Lms.Explore.Release.disc">Disc {1}</message>
|
<message id="Lms.Explore.Release.disc">Disc {1}</message>
|
||||||
<message id="Lms.Explore.Release.other-versions">Other versions</message>
|
<message id="Lms.Explore.Release.other-versions">Other versions</message>
|
||||||
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
|
<message id="Lms.Explore.Release.related-releases">Related albums</message>
|
||||||
<message id="Lms.Explore.Release.type">Type</message>
|
<message id="Lms.Explore.Release.type">Type</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-broadcast">Broadcast</message>
|
<message id="Lms.Explore.Release.type-primary-broadcast">Broadcast</message>
|
||||||
|
|||||||
+11
-7
@@ -94,13 +94,15 @@
|
|||||||
<message id="Lms.Admin.Database.misc">Varios</message>
|
<message id="Lms.Admin.Database.misc">Varios</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Mensualmente</message>
|
<message id="Lms.Admin.Database.monthly">Mensualmente</message>
|
||||||
<message id="Lms.Admin.Database.never">Nunca</message>
|
<message id="Lms.Admin.Database.never">Nunca</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-not-supported">Este motor no es compatible</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type">Motor de recomendaciones</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.audio-similarity">Similitud de audio</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Etiquetas</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.none">Ninguno</message>
|
||||||
<message id="Lms.Admin.Database.scan-aborted">¡Escaneo interrumpido!</message>
|
<message id="Lms.Admin.Database.scan-aborted">¡Escaneo interrumpido!</message>
|
||||||
<message id="Lms.Admin.Database.scan-complete">Escaneo terminado : {1} ficheros, {2} añadidos, {3} actualizados, {4} eliminados, {5} duplicados, {6} errores</message>
|
<message id="Lms.Admin.Database.scan-complete">Escaneo terminado : {1} ficheros, {2} añadidos, {3} actualizados, {4} eliminados, {5} duplicados, {6} errores</message>
|
||||||
<message id="Lms.Admin.Database.scan-launched">¡Escaneo comenzado!</message>
|
<message id="Lms.Admin.Database.scan-launched">¡Escaneo comenzado!</message>
|
||||||
<message id="Lms.Admin.Database.scan-settings">Opciones de escaneo</message>
|
<message id="Lms.Admin.Database.scan-settings">Opciones de escaneo</message>
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type">Motor de semejanza</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basado en las etiquetas</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.none">Ninguno</message>
|
|
||||||
<message id="Lms.Admin.Database.skip-single-release-playlists">Ignorar las lista de reproducción que contienen pistas del mismo álbum</message>
|
<message id="Lms.Admin.Database.skip-single-release-playlists">Ignorar las lista de reproducción que contienen pistas del mismo álbum</message>
|
||||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Los delimitadores de las etiquetas no deben consistir solo en espacios</message>
|
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Los delimitadores de las etiquetas no deben consistir solo en espacios</message>
|
||||||
<message id="Lms.Admin.Database.tag-parsing">Análisis de las etiquetas</message>
|
<message id="Lms.Admin.Database.tag-parsing">Análisis de las etiquetas</message>
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
|
||||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1} : {2}</message>
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1} : {2}</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.cannot-extract-musicnn-embeddings">No se pueden extraer los embeddings de MusicNN: {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero ({1})</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero ({1})</message>
|
||||||
@@ -148,9 +151,10 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracción de embeddings de MusicNN: {1} de {2} archivos ({3}%)</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message>
|
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Recargando el motor de similitud: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-reloading-recommendation-engine">Recargando el motor de recomendaciones: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Borrando entradas huérfanas: {1} entradas...</message>
|
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Borrando entradas huérfanas: {1} entradas...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Análisis de bibliotecas: {1} ficheros...</message>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">Análisis de bibliotecas: {1} ficheros...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">Estado de las etapas</message>
|
<message id="Lms.Admin.ScannerController.step-status">Estado de las etapas</message>
|
||||||
@@ -160,7 +164,7 @@
|
|||||||
<message id="Lms.Admin.DebugTools.debug-tools">Herramientas de depuración</message>
|
<message id="Lms.Admin.DebugTools.debug-tools">Herramientas de depuración</message>
|
||||||
|
|
||||||
<!--Db-->
|
<!--Db-->
|
||||||
<message id="Lms.Admin.DebugTools.Db.export-query-plans">Exportar planes de consulta</message>
|
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exportar datos de perfil de consultas</message>
|
||||||
<message id="Lms.Admin.DebugTools.Db.db">Base de datos</message>
|
<message id="Lms.Admin.DebugTools.Db.db">Base de datos</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
@@ -277,13 +281,13 @@
|
|||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.Artist.appears-on">Aparece en</message>
|
<message id="Lms.Explore.Artist.appears-on">Aparece en</message>
|
||||||
<message id="Lms.Explore.Artist.biography">Biografía</message>
|
<message id="Lms.Explore.Artist.biography">Biografía</message>
|
||||||
<message id="Lms.Explore.Artist.similar-artists">Artistas similares</message>
|
<message id="Lms.Explore.Artist.related-artists">Artistas relacionados</message>
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||||
<message id="Lms.Explore.Release.disc">Disco {1}</message>
|
<message id="Lms.Explore.Release.disc">Disco {1}</message>
|
||||||
<message id="Lms.Explore.Release.other-versions">Otras versiones</message>
|
<message id="Lms.Explore.Release.other-versions">Otras versiones</message>
|
||||||
<message id="Lms.Explore.Release.similar-releases">Álbumes similares</message>
|
<message id="Lms.Explore.Release.related-releases">Álbumes relacionados</message>
|
||||||
<message id="Lms.Explore.Release.type">Tipo</message>
|
<message id="Lms.Explore.Release.type">Tipo</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-album">Álbum</message>
|
<message id="Lms.Explore.Release.type-primary-album">Álbum</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-broadcast">Difusión</message>
|
<message id="Lms.Explore.Release.type-primary-broadcast">Difusión</message>
|
||||||
|
|||||||
+11
-7
@@ -94,13 +94,15 @@
|
|||||||
<message id="Lms.Admin.Database.misc">Divers</message>
|
<message id="Lms.Admin.Database.misc">Divers</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
||||||
<message id="Lms.Admin.Database.never">Jamais</message>
|
<message id="Lms.Admin.Database.never">Jamais</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-not-supported">Ce moteur n'est pas pris en charge</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type">Moteur de recommandation</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.audio-similarity">Similarité audio</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Tags</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.none">Aucun</message>
|
||||||
<message id="Lms.Admin.Database.scan-aborted">Scan interrompu !</message>
|
<message id="Lms.Admin.Database.scan-aborted">Scan interrompu !</message>
|
||||||
<message id="Lms.Admin.Database.scan-complete">Scan terminé : {1} fichiers, {2} ajouts, {3} mises à jour, {4} suppressions, {5} duplicatas, {6} erreurs</message>
|
<message id="Lms.Admin.Database.scan-complete">Scan terminé : {1} fichiers, {2} ajouts, {3} mises à jour, {4} suppressions, {5} duplicatas, {6} erreurs</message>
|
||||||
<message id="Lms.Admin.Database.scan-launched">Scan lancé !</message>
|
<message id="Lms.Admin.Database.scan-launched">Scan lancé !</message>
|
||||||
<message id="Lms.Admin.Database.scan-settings">Options </message>
|
<message id="Lms.Admin.Database.scan-settings">Options </message>
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type">Moteur de similarité</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basé sur les tags</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.none">Aucun</message>
|
|
||||||
<message id="Lms.Admin.Database.skip-single-release-playlists">Ignorer les playlists contenant des pistes d'un même album</message>
|
<message id="Lms.Admin.Database.skip-single-release-playlists">Ignorer les playlists contenant des pistes d'un même album</message>
|
||||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Le délimiteur de tag ne doit pas comporter uniquement des espaces</message>
|
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Le délimiteur de tag ne doit pas comporter uniquement des espaces</message>
|
||||||
<message id="Lms.Admin.Database.tag-parsing">Analyse des tags</message>
|
<message id="Lms.Admin.Database.tag-parsing">Analyse des tags</message>
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
|
||||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossible de lire l'image à l'indice {1} : {2}</message>
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossible de lire l'image à l'indice {1} : {2}</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.cannot-extract-musicnn-embeddings">Impossible d'extraire les embeddings MusicNN : {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier ({1})</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier ({1})</message>
|
||||||
@@ -148,9 +151,10 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extraction des embeddings MusicNN : {1} sur {2} fichiers ({3}%)</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message>
|
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-reloading-recommendation-engine">Rechargement du moteur de recommandation : {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Retrait des entrées orphelines: {1} entrées...</message>
|
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Retrait des entrées orphelines: {1} entrées...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Analyse des bibliothèques : {1} fichiers...</message>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">Analyse des bibliothèques : {1} fichiers...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
|
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
|
||||||
@@ -160,7 +164,7 @@
|
|||||||
<message id="Lms.Admin.DebugTools.debug-tools">Outils de débogage</message>
|
<message id="Lms.Admin.DebugTools.debug-tools">Outils de débogage</message>
|
||||||
|
|
||||||
<!--Db-->
|
<!--Db-->
|
||||||
<message id="Lms.Admin.DebugTools.Db.export-query-plans">Exporter les plans de requête</message>
|
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exporter les données de profilage des requêtes</message>
|
||||||
<message id="Lms.Admin.DebugTools.Db.db">Base de données</message>
|
<message id="Lms.Admin.DebugTools.Db.db">Base de données</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
@@ -277,13 +281,13 @@
|
|||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.Artist.appears-on">Apparaît dans</message>
|
<message id="Lms.Explore.Artist.appears-on">Apparaît dans</message>
|
||||||
<message id="Lms.Explore.Artist.biography">Biographie</message>
|
<message id="Lms.Explore.Artist.biography">Biographie</message>
|
||||||
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
|
<message id="Lms.Explore.Artist.related-artists">Artistes associés</message>
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||||
<message id="Lms.Explore.Release.disc">Disque {1}</message>
|
<message id="Lms.Explore.Release.disc">Disque {1}</message>
|
||||||
<message id="Lms.Explore.Release.other-versions">Autres versions</message>
|
<message id="Lms.Explore.Release.other-versions">Autres versions</message>
|
||||||
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message>
|
<message id="Lms.Explore.Release.related-releases">Albums associés</message>
|
||||||
<message id="Lms.Explore.Release.type">Type</message>
|
<message id="Lms.Explore.Release.type">Type</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-broadcast">Diffusion</message>
|
<message id="Lms.Explore.Release.type-primary-broadcast">Diffusion</message>
|
||||||
|
|||||||
+11
-7
@@ -94,13 +94,15 @@
|
|||||||
<message id="Lms.Admin.Database.misc">Varie</message>
|
<message id="Lms.Admin.Database.misc">Varie</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Mensile</message>
|
<message id="Lms.Admin.Database.monthly">Mensile</message>
|
||||||
<message id="Lms.Admin.Database.never">Mai</message>
|
<message id="Lms.Admin.Database.never">Mai</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-not-supported">Questo motore non è supportato</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type">Motore di raccomandazione</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.audio-similarity">Similarità audio</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Tag</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.none">Nessuno</message>
|
||||||
<message id="Lms.Admin.Database.scan-aborted">Scansione annullata!</message>
|
<message id="Lms.Admin.Database.scan-aborted">Scansione annullata!</message>
|
||||||
<message id="Lms.Admin.Database.scan-complete">Scansione completata: {1} file totali, {2} aggiunti, {3} aggiornati, {4} eliminati, {5} duplicati, {6} errati</message>
|
<message id="Lms.Admin.Database.scan-complete">Scansione completata: {1} file totali, {2} aggiunti, {3} aggiornati, {4} eliminati, {5} duplicati, {6} errati</message>
|
||||||
<message id="Lms.Admin.Database.scan-launched">Scansione avviata!</message>
|
<message id="Lms.Admin.Database.scan-launched">Scansione avviata!</message>
|
||||||
<message id="Lms.Admin.Database.scan-settings">Impostazioni di scansione</message>
|
<message id="Lms.Admin.Database.scan-settings">Impostazioni di scansione</message>
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type">Motore di similarità</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basato su tag</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.none">Nessuno</message>
|
|
||||||
<message id="Lms.Admin.Database.skip-single-release-playlists">Salta le playlist che contengono brani dello stesso album</message>
|
<message id="Lms.Admin.Database.skip-single-release-playlists">Salta le playlist che contengono brani dello stesso album</message>
|
||||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Il delimitatore del tag non deve consistere esclusivamente di spazi</message>
|
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Il delimitatore del tag non deve consistere esclusivamente di spazi</message>
|
||||||
<message id="Lms.Admin.Database.tag-parsing">Analisi dei tag</message>
|
<message id="Lms.Admin.Database.tag-parsing">Analisi dei tag</message>
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
|
||||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossibile leggere l'immagine all'indice {1} : {2}</message>
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossibile leggere l'immagine all'indice {1} : {2}</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.cannot-extract-musicnn-embeddings">Impossibile estrarre gli embeddings MusicNN: {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file ({1})</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file ({1})</message>
|
||||||
@@ -148,9 +151,10 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Estrazione degli embeddings MusicNN: {1} di {2} file ({3}%)</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message>
|
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-reloading-recommendation-engine">Ricaricamento del motore di raccomandazione: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Rimozione voci orfane: {1} voci...</message>
|
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Rimozione voci orfane: {1} voci...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Analisi delle librerie: {1} file...</message>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">Analisi delle librerie: {1} file...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
|
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
|
||||||
@@ -160,7 +164,7 @@
|
|||||||
<message id="Lms.Admin.DebugTools.debug-tools">Strumenti di debug</message>
|
<message id="Lms.Admin.DebugTools.debug-tools">Strumenti di debug</message>
|
||||||
|
|
||||||
<!--Db-->
|
<!--Db-->
|
||||||
<message id="Lms.Admin.DebugTools.Db.export-query-plans">Esporta i piani di query</message>
|
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Esporta i dati di profilazione delle query</message>
|
||||||
<message id="Lms.Admin.DebugTools.Db.db">Database</message>
|
<message id="Lms.Admin.DebugTools.Db.db">Database</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
@@ -277,13 +281,13 @@
|
|||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.Artist.appears-on">Appare su</message>
|
<message id="Lms.Explore.Artist.appears-on">Appare su</message>
|
||||||
<message id="Lms.Explore.Artist.biography">Biografia</message>
|
<message id="Lms.Explore.Artist.biography">Biografia</message>
|
||||||
<message id="Lms.Explore.Artist.similar-artists">Artisti simili</message>
|
<message id="Lms.Explore.Artist.related-artists">Artisti correlati</message>
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||||
<message id="Lms.Explore.Release.disc">Disco {1}</message>
|
<message id="Lms.Explore.Release.disc">Disco {1}</message>
|
||||||
<message id="Lms.Explore.Release.other-versions">Altre versioni</message>
|
<message id="Lms.Explore.Release.other-versions">Altre versioni</message>
|
||||||
<message id="Lms.Explore.Release.similar-releases">Album simili</message>
|
<message id="Lms.Explore.Release.related-releases">Album correlati</message>
|
||||||
<message id="Lms.Explore.Release.type">Tipo</message>
|
<message id="Lms.Explore.Release.type">Tipo</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-broadcast">Trasmissione</message>
|
<message id="Lms.Explore.Release.type-primary-broadcast">Trasmissione</message>
|
||||||
|
|||||||
+11
-7
@@ -95,13 +95,15 @@
|
|||||||
<message id="Lms.Admin.Database.misc">Różne</message>
|
<message id="Lms.Admin.Database.misc">Różne</message>
|
||||||
<message id="Lms.Admin.Database.monthly">Co miesiąc</message>
|
<message id="Lms.Admin.Database.monthly">Co miesiąc</message>
|
||||||
<message id="Lms.Admin.Database.never">Nigdy</message>
|
<message id="Lms.Admin.Database.never">Nigdy</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-not-supported">Ten silnik nie jest obsługiwany</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type">Silnik rekomendacji</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.audio-similarity">Podobieństwo audio</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Tagi</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.none">Żadna</message>
|
||||||
<message id="Lms.Admin.Database.scan-aborted">Skanowanie przerwane!</message>
|
<message id="Lms.Admin.Database.scan-aborted">Skanowanie przerwane!</message>
|
||||||
<message id="Lms.Admin.Database.scan-complete">Skanowanie ukończone: {1} wszystkich plików, {2} dodane, {3} zmienione, {4} usunięte, {5} duplikaty, {6} błędy</message>
|
<message id="Lms.Admin.Database.scan-complete">Skanowanie ukończone: {1} wszystkich plików, {2} dodane, {3} zmienione, {4} usunięte, {5} duplikaty, {6} błędy</message>
|
||||||
<message id="Lms.Admin.Database.scan-launched">Skanowanie uruchomione!</message>
|
<message id="Lms.Admin.Database.scan-launched">Skanowanie uruchomione!</message>
|
||||||
<message id="Lms.Admin.Database.scan-settings">Ustawienia skanowania</message>
|
<message id="Lms.Admin.Database.scan-settings">Ustawienia skanowania</message>
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type">Metoda sprawdzania podobieństwa</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Oparta o znaczniki</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.none">Żadna</message>
|
|
||||||
<message id="Lms.Admin.Database.skip-single-release-playlists">Pomiń playlisty zawierające utwory z tego samego albumu</message>
|
<message id="Lms.Admin.Database.skip-single-release-playlists">Pomiń playlisty zawierające utwory z tego samego albumu</message>
|
||||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Rozdzielacz nie może się składać z samych białych znaków</message>
|
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Rozdzielacz nie może się składać z samych białych znaków</message>
|
||||||
<message id="Lms.Admin.Database.tag-parsing">Analiza tagów</message>
|
<message id="Lms.Admin.Database.tag-parsing">Analiza tagów</message>
|
||||||
@@ -112,6 +114,7 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
|
||||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">Nie można odczytać obrazu pod indeksem {1} : {2}</message>
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">Nie można odczytać obrazu pod indeksem {1} : {2}</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.cannot-extract-musicnn-embeddings">Nie można wyodrębnić embeddingów MusicNN: {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku ({1})</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku ({1})</message>
|
||||||
@@ -161,9 +164,10 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Ekstrakcja embeddingów MusicNN: {1} z {2} plików ({3}%)</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message>
|
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-reloading-recommendation-engine">Przeładowywanie silnika rekomendacji: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Usuwanie osieroconych wpisów: {1} wpisów...</message>
|
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Usuwanie osieroconych wpisów: {1} wpisów...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">
|
<message id="Lms.Admin.ScannerController.step-scanning-files">
|
||||||
<plural case="0">Analiza bibliotek: {1} plik</plural>
|
<plural case="0">Analiza bibliotek: {1} plik</plural>
|
||||||
@@ -177,7 +181,7 @@
|
|||||||
<message id="Lms.Admin.DebugTools.debug-tools">Narzędzia debugowania</message>
|
<message id="Lms.Admin.DebugTools.debug-tools">Narzędzia debugowania</message>
|
||||||
|
|
||||||
<!--Db-->
|
<!--Db-->
|
||||||
<message id="Lms.Admin.DebugTools.Db.export-query-plans">Eksportuj plany zapytań</message>
|
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Eksportuj dane profilowania zapytań</message>
|
||||||
<message id="Lms.Admin.DebugTools.Db.db">Baza danych</message>
|
<message id="Lms.Admin.DebugTools.Db.db">Baza danych</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
@@ -303,13 +307,13 @@
|
|||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.Artist.appears-on">Pojawia się na</message>
|
<message id="Lms.Explore.Artist.appears-on">Pojawia się na</message>
|
||||||
<message id="Lms.Explore.Artist.biography">Biografia</message>
|
<message id="Lms.Explore.Artist.biography">Biografia</message>
|
||||||
<message id="Lms.Explore.Artist.similar-artists">Podobni artyści</message>
|
<message id="Lms.Explore.Artist.related-artists">Powiązani artyści</message>
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.Release.copyright">Prawa autorskie</message>
|
<message id="Lms.Explore.Release.copyright">Prawa autorskie</message>
|
||||||
<message id="Lms.Explore.Release.disc">Dysk {1}</message>
|
<message id="Lms.Explore.Release.disc">Dysk {1}</message>
|
||||||
<message id="Lms.Explore.Release.other-versions">Inne wersje</message>
|
<message id="Lms.Explore.Release.other-versions">Inne wersje</message>
|
||||||
<message id="Lms.Explore.Release.similar-releases">Podobne albumy</message>
|
<message id="Lms.Explore.Release.related-releases">Powiązane albumy</message>
|
||||||
<message id="Lms.Explore.Release.type">Typ</message>
|
<message id="Lms.Explore.Release.type">Typ</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
<message id="Lms.Explore.Release.type-primary-album">Album</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-broadcast">Audycja</message>
|
<message id="Lms.Explore.Release.type-primary-broadcast">Audycja</message>
|
||||||
|
|||||||
+11
-7
@@ -92,13 +92,15 @@
|
|||||||
<message id="Lms.Admin.Database.misc">其他</message>
|
<message id="Lms.Admin.Database.misc">其他</message>
|
||||||
<message id="Lms.Admin.Database.monthly">每月</message>
|
<message id="Lms.Admin.Database.monthly">每月</message>
|
||||||
<message id="Lms.Admin.Database.never">从不</message>
|
<message id="Lms.Admin.Database.never">从不</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-not-supported">此引擎不受支持</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type">推荐引擎</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.audio-similarity">音频相似性</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">标签</message>
|
||||||
|
<message id="Lms.Admin.Database.recommendation-engine-type.none">无</message>
|
||||||
<message id="Lms.Admin.Database.scan-aborted">扫描已中止!</message>
|
<message id="Lms.Admin.Database.scan-aborted">扫描已中止!</message>
|
||||||
<message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},重复文件 {5},错误文件 {6}</message>
|
<message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},重复文件 {5},错误文件 {6}</message>
|
||||||
<message id="Lms.Admin.Database.scan-launched">扫描已完成!</message>
|
<message id="Lms.Admin.Database.scan-launched">扫描已完成!</message>
|
||||||
<message id="Lms.Admin.Database.scan-settings">扫描选项</message>
|
<message id="Lms.Admin.Database.scan-settings">扫描选项</message>
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type">相似度引擎</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">基于标签</message>
|
|
||||||
<message id="Lms.Admin.Database.similarity-engine-type.none">无</message>
|
|
||||||
<message id="Lms.Admin.Database.skip-single-release-playlists">跳过仅包含同一专辑曲目的播放列表</message>
|
<message id="Lms.Admin.Database.skip-single-release-playlists">跳过仅包含同一专辑曲目的播放列表</message>
|
||||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">标签分隔符不能仅由空格组成</message>
|
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">标签分隔符不能仅由空格组成</message>
|
||||||
<message id="Lms.Admin.Database.tag-parsing">标签解析</message>
|
<message id="Lms.Admin.Database.tag-parsing">标签解析</message>
|
||||||
@@ -109,6 +111,7 @@
|
|||||||
<!--Scanner Controller-->
|
<!--Scanner Controller-->
|
||||||
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
||||||
<message id="Lms.Admin.ScannerController.bad-embedded-image">无法读取索引 {1} 处的图片:{2}</message>
|
<message id="Lms.Admin.ScannerController.bad-embedded-image">无法读取索引 {1} 处的图片:{2}</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.cannot-extract-musicnn-embeddings">无法提取 MusicNN 嵌入:{1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">无法解析艺术家信息文件</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">无法解析艺术家信息文件</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
|
||||||
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件({1})</message>
|
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件({1})</message>
|
||||||
@@ -146,9 +149,10 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">正在压缩数据库...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">正在压缩数据库...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">正在计算统计信息... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">正在计算统计信息... {1}%</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">正在提取 MusicNN 嵌入:{1} / {2} 个文件({3}%)</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-optimize">正在优化数据库... {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-optimize">正在优化数据库... {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">整理艺术家:{1} 条记录...</message>
|
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">整理艺术家:{1} 条记录...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-reloading-recommendation-engine">正在重新加载推荐引擎:{1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">移除孤立条目:{1} 条记录...</message>
|
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">移除孤立条目:{1} 条记录...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">库分析:{1} 个文件...</message>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">库分析:{1} 个文件...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
|
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
|
||||||
@@ -158,7 +162,7 @@
|
|||||||
<message id="Lms.Admin.DebugTools.debug-tools">调试工具</message>
|
<message id="Lms.Admin.DebugTools.debug-tools">调试工具</message>
|
||||||
|
|
||||||
<!--Db-->
|
<!--Db-->
|
||||||
<message id="Lms.Admin.DebugTools.Db.export-query-plans">导出查询计划</message>
|
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">导出查询分析数据</message>
|
||||||
<message id="Lms.Admin.DebugTools.Db.db">数据库</message>
|
<message id="Lms.Admin.DebugTools.Db.db">数据库</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
@@ -248,13 +252,13 @@
|
|||||||
<!--Explore:Artist-->
|
<!--Explore:Artist-->
|
||||||
<message id="Lms.Explore.Artist.appears-on">出现于</message>
|
<message id="Lms.Explore.Artist.appears-on">出现于</message>
|
||||||
<message id="Lms.Explore.Artist.biography">简介</message>
|
<message id="Lms.Explore.Artist.biography">简介</message>
|
||||||
<message id="Lms.Explore.Artist.similar-artists">相似歌手</message>
|
<message id="Lms.Explore.Artist.related-artists">相关歌手</message>
|
||||||
|
|
||||||
<!--Explore:Release-->
|
<!--Explore:Release-->
|
||||||
<message id="Lms.Explore.Release.copyright">版权所有</message>
|
<message id="Lms.Explore.Release.copyright">版权所有</message>
|
||||||
<message id="Lms.Explore.Release.disc">唱片 {1}</message>
|
<message id="Lms.Explore.Release.disc">唱片 {1}</message>
|
||||||
<message id="Lms.Explore.Release.other-versions">其他版本</message>
|
<message id="Lms.Explore.Release.other-versions">其他版本</message>
|
||||||
<message id="Lms.Explore.Release.similar-releases">相似专辑</message>
|
<message id="Lms.Explore.Release.related-releases">相关专辑</message>
|
||||||
<message id="Lms.Explore.Release.type">类型</message>
|
<message id="Lms.Explore.Release.type">类型</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-album">专辑</message>
|
<message id="Lms.Explore.Release.type-primary-album">专辑</message>
|
||||||
<message id="Lms.Explore.Release.type-primary-broadcast">广播</message>
|
<message id="Lms.Explore.Release.type-primary-broadcast">广播</message>
|
||||||
|
|||||||
+4
-4
@@ -45,10 +45,10 @@
|
|||||||
<h3>${tr:Lms.Explore.Release.other-versions}</h3>
|
<h3>${tr:Lms.Explore.Release.other-versions}</h3>
|
||||||
${other-versions class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
|
${other-versions class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
|
||||||
${</if-has-other-versions>}
|
${</if-has-other-versions>}
|
||||||
${<if-has-similar-releases>}
|
${<if-has-related-releases>}
|
||||||
<h3>${tr:Lms.Explore.Release.similar-releases}</h3>
|
<h3>${tr:Lms.Explore.Release.related-releases}</h3>
|
||||||
${similar-releases class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
|
${related-releases class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 gx-2 gy-4"}
|
||||||
${</if-has-similar-releases>}
|
${</if-has-related-releases>}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Release.template.entry-release-artist">
|
<message id="Lms.Explore.Release.template.entry-release-artist">
|
||||||
|
|||||||
+8
-2
@@ -18,9 +18,9 @@ db-integrity-check = "quick";
|
|||||||
# Output db queries on stdout
|
# Output db queries on stdout
|
||||||
# Use this only for debugging purpose, as this may impact performance
|
# Use this only for debugging purpose, as this may impact performance
|
||||||
db-show-queries = false;
|
db-show-queries = false;
|
||||||
# Record query plans for database queries.
|
# Record stats for database queries.
|
||||||
# Use this only for debugging purposes, as this may impact performance
|
# Use this only for debugging purposes, as this may impact performance
|
||||||
db-record-query-plans = false;
|
db-profile-queries = false;
|
||||||
|
|
||||||
# Listen port/addr of the web server
|
# Listen port/addr of the web server
|
||||||
listen-port = 5082;
|
listen-port = 5082;
|
||||||
@@ -129,6 +129,12 @@ scanner-parser-read-style = "average";
|
|||||||
# Number of threads to use for parallelized tasks (e.g., scanning file metadata). 0 means half the number of logical CPUs.
|
# Number of threads to use for parallelized tasks (e.g., scanning file metadata). 0 means half the number of logical CPUs.
|
||||||
scanner-thread-count = 0;
|
scanner-thread-count = 0;
|
||||||
|
|
||||||
|
# Path to the musicNN model
|
||||||
|
musicnn-model-path = "/usr/share/lms/models/MSD_musicnn_embedding.onnx";
|
||||||
|
|
||||||
|
# Maximum number of non-overlapping patches to extract per track. More patches means better accuracy but also longer processing time. Must be > 0.
|
||||||
|
musicnn-max-patch-count-per-track = 20;
|
||||||
|
|
||||||
# Refresh period for podcast feeds in hours (must be greater or equal than 1)
|
# Refresh period for podcast feeds in hours (must be greater or equal than 1)
|
||||||
podcast-refresh-period-hours = 2;
|
podcast-refresh-period-hours = 2;
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
|
# Want as many meaningful warnings as possible
|
||||||
add_compile_options(-Wall -Wextra -pedantic)
|
add_compile_options(-Wall -Wextra -pedantic)
|
||||||
|
|
||||||
add_subdirectory(libs)
|
add_subdirectory(libs)
|
||||||
add_subdirectory(lms)
|
add_subdirectory(lms)
|
||||||
add_subdirectory(tools)
|
add_subdirectory(tools)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ add_subdirectory(audio)
|
|||||||
add_subdirectory(core)
|
add_subdirectory(core)
|
||||||
add_subdirectory(database)
|
add_subdirectory(database)
|
||||||
add_subdirectory(image)
|
add_subdirectory(image)
|
||||||
|
add_subdirectory(math)
|
||||||
add_subdirectory(services)
|
add_subdirectory(services)
|
||||||
add_subdirectory(som)
|
|
||||||
add_subdirectory(subsonic)
|
add_subdirectory(subsonic)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat libswre
|
|||||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||||
pkg_check_modules(PulseAudio IMPORTED_TARGET libpulse)
|
pkg_check_modules(PulseAudio IMPORTED_TARGET libpulse)
|
||||||
pkg_check_modules(ALSA IMPORTED_TARGET alsa)
|
pkg_check_modules(ALSA IMPORTED_TARGET alsa)
|
||||||
|
pkg_check_modules(OnnxRuntime IMPORTED_TARGET libonnxruntime)
|
||||||
|
|
||||||
if (PulseAudio_FOUND OR ALSA_FOUND)
|
if (PulseAudio_FOUND OR ALSA_FOUND)
|
||||||
message(STATUS "Audio output available (PulseAudio=${PulseAudio_FOUND}, ALSA=${ALSA_FOUND})")
|
message(STATUS "Audio output available (PulseAudio=${PulseAudio_FOUND}, ALSA=${ALSA_FOUND})")
|
||||||
@@ -10,6 +11,7 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_library(lmsaudio STATIC
|
add_library(lmsaudio STATIC
|
||||||
|
impl/features/MelFilterBank.cpp
|
||||||
impl/ffmpeg/AudioFile.cpp
|
impl/ffmpeg/AudioFile.cpp
|
||||||
impl/ffmpeg/AudioFileInfo.cpp
|
impl/ffmpeg/AudioFileInfo.cpp
|
||||||
impl/ffmpeg/AudioFileInfoParser.cpp
|
impl/ffmpeg/AudioFileInfoParser.cpp
|
||||||
@@ -19,6 +21,7 @@ add_library(lmsaudio STATIC
|
|||||||
impl/ffmpeg/TagReader.cpp
|
impl/ffmpeg/TagReader.cpp
|
||||||
impl/ffmpeg/Transcoder.cpp
|
impl/ffmpeg/Transcoder.cpp
|
||||||
impl/ffmpeg/Utils.cpp
|
impl/ffmpeg/Utils.cpp
|
||||||
|
impl/musicnn/MusicNNEmbeddings.cpp
|
||||||
impl/taglib/AudioFileInfo.cpp
|
impl/taglib/AudioFileInfo.cpp
|
||||||
impl/taglib/AudioFileInfoParser.cpp
|
impl/taglib/AudioFileInfoParser.cpp
|
||||||
impl/taglib/ImageReader.cpp
|
impl/taglib/ImageReader.cpp
|
||||||
@@ -27,6 +30,7 @@ add_library(lmsaudio STATIC
|
|||||||
impl/utils/PcmDecodeStreamer.cpp
|
impl/utils/PcmDecodeStreamer.cpp
|
||||||
impl/AudioFileInfoParser.cpp
|
impl/AudioFileInfoParser.cpp
|
||||||
impl/AudioOutput.cpp
|
impl/AudioOutput.cpp
|
||||||
|
impl/MusicNNEmbeddingExtractorCreator.cpp
|
||||||
impl/PcmTypes.cpp
|
impl/PcmTypes.cpp
|
||||||
impl/TagReader.cpp
|
impl/TagReader.cpp
|
||||||
)
|
)
|
||||||
@@ -37,6 +41,7 @@ target_include_directories(lmsaudio INTERFACE
|
|||||||
|
|
||||||
target_include_directories(lmsaudio PRIVATE
|
target_include_directories(lmsaudio PRIVATE
|
||||||
include
|
include
|
||||||
|
impl
|
||||||
${AVCODEC_INCLUDE_DIR}
|
${AVCODEC_INCLUDE_DIR}
|
||||||
${AVFORMAT_INCLUDE_DIR}
|
${AVFORMAT_INCLUDE_DIR}
|
||||||
${AVUTIL_INCLUDE_DIR}
|
${AVUTIL_INCLUDE_DIR}
|
||||||
@@ -47,6 +52,7 @@ target_link_libraries(lmsaudio PUBLIC
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lmsaudio PRIVATE
|
target_link_libraries(lmsaudio PRIVATE
|
||||||
|
lmsmath
|
||||||
PkgConfig::LIBAV
|
PkgConfig::LIBAV
|
||||||
PkgConfig::Taglib
|
PkgConfig::Taglib
|
||||||
)
|
)
|
||||||
@@ -54,6 +60,12 @@ target_link_libraries(lmsaudio PRIVATE
|
|||||||
target_compile_definitions(lmsaudio PRIVATE
|
target_compile_definitions(lmsaudio PRIVATE
|
||||||
$<$<BOOL:${PulseAudio_FOUND}>:LMS_HAVE_PULSEAUDIO>
|
$<$<BOOL:${PulseAudio_FOUND}>:LMS_HAVE_PULSEAUDIO>
|
||||||
$<$<BOOL:${ALSA_FOUND}>:LMS_HAVE_ALSA>
|
$<$<BOOL:${ALSA_FOUND}>:LMS_HAVE_ALSA>
|
||||||
|
$<$<BOOL:${OnnxRuntime_FOUND}>:LMS_HAVE_ONNX_RUNTIME>
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be safe enough for what we're doing
|
||||||
|
target_compile_options(lmsaudio PRIVATE
|
||||||
|
$<$<NOT:$<CONFIG:Debug>>:-ffast-math>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (PulseAudio_FOUND)
|
if (PulseAudio_FOUND)
|
||||||
@@ -80,3 +92,21 @@ if (ALSA_FOUND)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (OnnxRuntime_FOUND)
|
||||||
|
target_sources(lmsaudio PRIVATE
|
||||||
|
impl/musicnn/MusicNNEmbeddingExtractor.cpp
|
||||||
|
impl/musicnn/MusicNNModel.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(lmsaudio PRIVATE PkgConfig::OnnxRuntime)
|
||||||
|
message(STATUS "Using ONNX Runtime (${OnnxRuntime_VERSION})")
|
||||||
|
|
||||||
|
install(DIRECTORY models DESTINATION share/lms)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_subdirectory(test)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (BUILD_BENCHMARKS)
|
||||||
|
add_subdirectory(bench)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -17,10 +17,6 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
BENCHMARK_MAIN();
|
||||||
{
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
|
||||||
return RUN_ALL_TESTS();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
add_executable(bench-audio
|
||||||
|
Audio.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (OnnxRuntime_FOUND)
|
||||||
|
target_sources(bench-audio PRIVATE
|
||||||
|
MusicNNModel.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(bench-audio PRIVATE
|
||||||
|
../impl
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(bench-audio PRIVATE
|
||||||
|
lmsaudio
|
||||||
|
benchmark
|
||||||
|
)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "musicnn/MusicNNModel.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::musicnn::benchmarks
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path getMusicNNModelPathFromEnv()
|
||||||
|
{
|
||||||
|
const char* p{ std::getenv("LMS_MUSICNN_MODEL") };
|
||||||
|
return p ? std::filesystem::path{ p } : std::filesystem::path{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<float, MusicNNModel::inputFrames * MusicNNModel::inputBands> makeRandomPatch()
|
||||||
|
{
|
||||||
|
std::minstd_rand rng{ 42 };
|
||||||
|
std::uniform_real_distribution<float> dist{ 0.F, 1.F };
|
||||||
|
std::array<float, MusicNNModel::inputFrames * MusicNNModel::inputBands> patch{};
|
||||||
|
for (float& v : patch)
|
||||||
|
v = dist(rng);
|
||||||
|
return patch;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static void BM_MusicNNModel_forward(benchmark::State& state)
|
||||||
|
{
|
||||||
|
const std::filesystem::path path{ getMusicNNModelPathFromEnv() };
|
||||||
|
if (path.empty())
|
||||||
|
{
|
||||||
|
state.SkipWithMessage("LMS_MUSICNN_MODEL not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicNNModel model{ path };
|
||||||
|
const auto patch{ makeRandomPatch() };
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
benchmark::DoNotOptimize(model.forward(patch));
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK(BM_MusicNNModel_forward);
|
||||||
|
|
||||||
|
} // namespace lms::audio::musicnn::benchmarks
|
||||||
@@ -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" };
|
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
|
} // 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
|
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)
|
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||||
return true;
|
return true;
|
||||||
@@ -304,7 +304,7 @@ namespace lms::audio::ffmpeg
|
|||||||
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
|
{ 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];
|
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) };
|
_decoderContext = AVCodecContextPtr{ ::avcodec_alloc_context3(decoder) };
|
||||||
if (!_decoderContext)
|
if (!_decoderContext)
|
||||||
throw Exception{ "Cannot allocate decoder context" };
|
throw Exception{ "Cannot allocate decoder context" };
|
||||||
@@ -225,7 +233,7 @@ namespace lms::audio::ffmpeg
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{};
|
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());
|
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data());
|
||||||
|
|
||||||
// Resample decoded audio
|
// Resample decoded audio
|
||||||
@@ -266,6 +274,11 @@ namespace lms::audio::ffmpeg
|
|||||||
return _finished;
|
return _finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::chrono::milliseconds PcmDecoder::getEstimatedDuration() const
|
||||||
|
{
|
||||||
|
return _estimatedDuration;
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t PcmDecoder::computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const
|
std::size_t PcmDecoder::computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const
|
||||||
{
|
{
|
||||||
if (_parameters.planar)
|
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::size_t PcmDecoder::drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel)
|
||||||
{
|
{
|
||||||
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{};
|
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());
|
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data());
|
||||||
|
|
||||||
const int outSampleCount{ ::swr_convert(_resampleContext.get(),
|
const int outSampleCount{ ::swr_convert(_resampleContext.get(),
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ namespace lms::audio::ffmpeg
|
|||||||
std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) override;
|
std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) override;
|
||||||
bool finished() const override;
|
bool finished() const override;
|
||||||
|
|
||||||
|
std::chrono::milliseconds getEstimatedDuration() const override;
|
||||||
|
|
||||||
std::size_t computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const;
|
std::size_t computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const;
|
||||||
void feedDecoder();
|
void feedDecoder();
|
||||||
std::size_t drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel);
|
std::size_t drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel);
|
||||||
@@ -52,6 +54,7 @@ namespace lms::audio::ffmpeg
|
|||||||
bool _draining{};
|
bool _draining{};
|
||||||
|
|
||||||
AVFormatContextPtr _context;
|
AVFormatContextPtr _context;
|
||||||
|
std::chrono::milliseconds _estimatedDuration{};
|
||||||
int _inputStreamIndex{};
|
int _inputStreamIndex{};
|
||||||
AVCodecContextPtr _decoderContext;
|
AVCodecContextPtr _decoderContext;
|
||||||
AVFramePtr _decodedFrame;
|
AVFramePtr _decodedFrame;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ namespace lms::audio::ffmpeg::utils
|
|||||||
|
|
||||||
void avLogCallback(void*, int level, const char* fmt, va_list vl)
|
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;
|
return;
|
||||||
|
|
||||||
if (level > AV_LOG_WARNING)
|
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
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <boost/asio/io_context.hpp>
|
#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
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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 <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "audio/MusicNNEmbeddings.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio
|
||||||
|
{
|
||||||
|
class IMusicNNEmbeddingExtractor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IMusicNNEmbeddingExtractor() = default;
|
||||||
|
|
||||||
|
struct ExtractionResult
|
||||||
|
{
|
||||||
|
TrackMusicNNEmbeddings embeddings{};
|
||||||
|
std::size_t patchCount{};
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] virtual ExtractionResult extract(const std::filesystem::path& audioFile) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool canExtractMusicNNEmbeddings();
|
||||||
|
std::unique_ptr<IMusicNNEmbeddingExtractor> createMusicNNEmbeddingExtractor(const std::filesystem::path& modelPath, std::size_t maxPatchCount);
|
||||||
|
std::string getMusicNNModelIdentifier(const std::filesystem::path& modelPath);
|
||||||
|
} // namespace lms::audio
|
||||||
@@ -43,8 +43,11 @@ namespace lms::audio
|
|||||||
// Each buffer must be sized to hold an integer number of samples according to the requested sample type.
|
// Each buffer must be sized to hold an integer number of samples according to the requested sample type.
|
||||||
// For example, for Float32 planar output, each buffer size must be divisible by sizeof(float).
|
// For example, for Float32 planar output, each buffer size must be divisible by sizeof(float).
|
||||||
// The decoder will use the buffer sizes to determine the maximum number of samples it can write.
|
// The decoder will use the buffer sizes to determine the maximum number of samples it can write.
|
||||||
|
// The decoder will not try to fill in the whole supplied buffer
|
||||||
virtual std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) = 0;
|
virtual std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) = 0;
|
||||||
virtual bool finished() const = 0;
|
virtual bool finished() const = 0;
|
||||||
|
|
||||||
|
virtual std::chrono::milliseconds getEstimatedDuration() const = 0; // initial offset is taken into account, 0 if unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
// Throw on error
|
// Throw on error
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 <span>
|
||||||
|
|
||||||
|
namespace lms::audio
|
||||||
|
{
|
||||||
|
struct MusicNNEmbedding
|
||||||
|
{
|
||||||
|
static inline constexpr std::size_t size{ 200 };
|
||||||
|
std::array<float, size> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TrackMusicNNEmbeddings
|
||||||
|
{
|
||||||
|
MusicNNEmbedding mean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buffer size must be at least sizeof(TrackMusicNNEmbeddings)
|
||||||
|
void trackMusicNNEmbeddingsToBlob(const TrackMusicNNEmbeddings& embeddings, std::span<std::byte> buffer);
|
||||||
|
void trackMusicNNEmbeddingsFromBlob(std::span<const std::byte> buffer, TrackMusicNNEmbeddings& embeddings);
|
||||||
|
} // namespace lms::audio
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <bit>
|
#include <bit>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace lms::audio
|
namespace lms::audio
|
||||||
{
|
{
|
||||||
@@ -41,4 +42,21 @@ namespace lms::audio
|
|||||||
std::endian byteOrder;
|
std::endian byteOrder;
|
||||||
bool planar;
|
bool planar;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
namespace helpers
|
||||||
|
{
|
||||||
|
template<typename Rep, typename Period>
|
||||||
|
std::size_t durationToSampleCount(std::chrono::duration<Rep, Period> duration, unsigned sampleRate)
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(duration.count() * sampleRate * Period::num / Period::den);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Duration>
|
||||||
|
Duration sampleCountToDuration(std::size_t sampleCount, unsigned sampleRate)
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<Duration>(std::chrono::duration<double>(sampleCount) / sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t sampleCountToByteCount(std::size_t sampleCount, PcmSampleType sampleType, unsigned channelCount);
|
||||||
|
} // namespace helpers
|
||||||
} // namespace lms::audio
|
} // namespace lms::audio
|
||||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
|||||||
|
include(GoogleTest)
|
||||||
|
|
||||||
|
add_executable(test-audio
|
||||||
|
MelFilterBank.cpp
|
||||||
|
MusicNNEmbeddings.cpp
|
||||||
|
PcmSpectralFrameDecoder.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (OnnxRuntime_FOUND)
|
||||||
|
target_sources(test-audio PRIVATE
|
||||||
|
MusicNNModel.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_include_directories(test-audio PRIVATE
|
||||||
|
../impl
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(test-audio PRIVATE
|
||||||
|
lmsaudio
|
||||||
|
lmsmath
|
||||||
|
GTest::GTest
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_options(test-audio PRIVATE
|
||||||
|
$<$<NOT:$<CONFIG:Debug>>:-ffast-math>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (NOT CMAKE_CROSSCOMPILING)
|
||||||
|
gtest_discover_tests(test-audio)
|
||||||
|
endif()
|
||||||
|
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* 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 <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
|
|
||||||
|
#include "features/MelFilterBank.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::features::tests
|
||||||
|
{
|
||||||
|
constexpr float epsilon{ 1e-5F };
|
||||||
|
|
||||||
|
constexpr std::size_t NFFT{ 2048 };
|
||||||
|
constexpr std::size_t sampleRate{ 22050 };
|
||||||
|
constexpr std::size_t filterCount{ 40 };
|
||||||
|
|
||||||
|
TEST(MelFilterBank, sizeCheck)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
EXPECT_EQ(bank.getFilterCount(), filterCount);
|
||||||
|
EXPECT_EQ(bank.getBinCount(), NFFT / 2 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, differentSampleRates)
|
||||||
|
{
|
||||||
|
for (const std::size_t sr : { std::size_t{ 8000 }, std::size_t{ 16000 }, std::size_t{ 44100 }, std::size_t{ 48000 } })
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sr, filterCount) };
|
||||||
|
EXPECT_EQ(bank.getFilterCount(), filterCount) << "sr=" << sr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, nonNegativeWeights)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const auto& filter = bank.getFilter(m);
|
||||||
|
for (float w : filter.weights)
|
||||||
|
EXPECT_GE(w, 0.F) << "Filter " << m << " has negative weight: " << w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, peaksAreNonZero)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const auto& filter{ bank.getFilter(m) };
|
||||||
|
const float maxVal{ *std::max_element(filter.weights.begin(), filter.weights.end()) };
|
||||||
|
|
||||||
|
EXPECT_GT(maxVal, 0.F) << "Filter " << m << " has zero peak value";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, filtersAreUnitSum)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const auto& filter{ bank.getFilter(m) };
|
||||||
|
const float sum{ std::accumulate(filter.weights.begin(), filter.weights.end(), 0.F) };
|
||||||
|
|
||||||
|
EXPECT_NEAR(sum, 1.F, epsilon) << "Filter " << m << " sum = " << sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, everyBinCovered)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
const std::size_t binCount{ bank.getBinCount() };
|
||||||
|
|
||||||
|
std::vector<bool> covered(binCount, false);
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const auto& filter{ bank.getFilter(m) };
|
||||||
|
|
||||||
|
std::size_t bin{ filter.leftBinIndex };
|
||||||
|
for (float w : filter.weights)
|
||||||
|
{
|
||||||
|
if (w > 0.F)
|
||||||
|
covered[bin] = true;
|
||||||
|
|
||||||
|
++bin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find actual covered range
|
||||||
|
auto first{ std::find(covered.begin(), covered.end(), true) };
|
||||||
|
auto last{ std::find(covered.rbegin(), covered.rend(), true).base() };
|
||||||
|
|
||||||
|
ASSERT_NE(first, covered.end()); // sanity
|
||||||
|
|
||||||
|
for (auto it = first; it != last; ++it)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(*it) << "A bin in the covered range is not covered by any filter";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, overlapAtMostTwo)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
const std::size_t binCount{ bank.getBinCount() };
|
||||||
|
|
||||||
|
std::vector<std::size_t> overlap(binCount, 0);
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const auto& filter{ bank.getFilter(m) };
|
||||||
|
|
||||||
|
std::size_t bin{ filter.leftBinIndex };
|
||||||
|
for (float w : filter.weights)
|
||||||
|
{
|
||||||
|
if (w > 0.F)
|
||||||
|
overlap[bin]++;
|
||||||
|
++bin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t k{}; k < binCount; ++k)
|
||||||
|
{
|
||||||
|
EXPECT_LE(overlap[k], 2) << "Bin " << k << " is covered by " << overlap[k] << " filters";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, flatSpectrumEnergySanity)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
std::vector<float> flatSpectrum(bank.getBinCount(), 1.F);
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const float energy{ bank.computeEnergy(m, flatSpectrum) };
|
||||||
|
EXPECT_GT(energy, 0.F) << "Filter " << m << " has zero energy for flat spectrum";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TEST(MelFilterBank, zeroFilterCount)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, 0) };
|
||||||
|
|
||||||
|
EXPECT_EQ(bank.getFilterCount(), 0U);
|
||||||
|
EXPECT_EQ(bank.getBinCount(), NFFT / 2 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, computeEnergyRejectsInvalidInputSize)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
std::vector<float> invalidInput(bank.getBinCount() - 1, 1.F);
|
||||||
|
|
||||||
|
EXPECT_THROW(bank.computeEnergy(0, invalidInput), Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, zeroSpectrum)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
std::vector<float> zeroSpectrum(bank.getBinCount(), 0.F);
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const float energy{ bank.computeEnergy(m, zeroSpectrum) };
|
||||||
|
EXPECT_FLOAT_EQ(energy, 0.F) << "Filter " << m << " should have zero energy for zero spectrum";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MelFilterBank, largeSpectrumValues)
|
||||||
|
{
|
||||||
|
const MelFilterBank bank{ computeMelFilterBank(NFFT, sampleRate, filterCount) };
|
||||||
|
// Weights sum to 1.0 per filter, so energy = input * 1.0, no overflow risk at max/2
|
||||||
|
const float largeValue{ std::numeric_limits<float>::max() / 2.F };
|
||||||
|
std::vector<float> largeSpectrum(bank.getBinCount(), largeValue);
|
||||||
|
|
||||||
|
for (std::size_t m{}; m < bank.getFilterCount(); ++m)
|
||||||
|
{
|
||||||
|
const float energy{ bank.computeEnergy(m, largeSpectrum) };
|
||||||
|
EXPECT_GT(energy, 0.F) << "Filter " << m << " energy is not positive for large spectrum";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::features::tests
|
||||||
@@ -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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <numeric>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "audio/Exception.hpp"
|
||||||
|
#include "audio/MusicNNEmbeddings.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::tests
|
||||||
|
{
|
||||||
|
TEST(MusicNNEmbeddings, blobRoundTrip)
|
||||||
|
{
|
||||||
|
TrackMusicNNEmbeddings original{};
|
||||||
|
|
||||||
|
std::iota(original.mean.values.begin(), original.mean.values.end(), 0.F);
|
||||||
|
|
||||||
|
std::vector<std::byte> blob(sizeof(TrackMusicNNEmbeddings));
|
||||||
|
trackMusicNNEmbeddingsToBlob(original, blob);
|
||||||
|
|
||||||
|
TrackMusicNNEmbeddings restored{};
|
||||||
|
trackMusicNNEmbeddingsFromBlob(blob, restored);
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < MusicNNEmbedding::size; ++i)
|
||||||
|
EXPECT_FLOAT_EQ(restored.mean.values[i], original.mean.values[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MusicNNEmbeddings, blobSizeTooSmallThrows)
|
||||||
|
{
|
||||||
|
TrackMusicNNEmbeddings embeddings{};
|
||||||
|
std::vector<std::byte> blob(sizeof(TrackMusicNNEmbeddings) - 1);
|
||||||
|
EXPECT_THROW(trackMusicNNEmbeddingsToBlob(embeddings, blob), Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MusicNNEmbeddings, blobFromSizeTooSmallThrows)
|
||||||
|
{
|
||||||
|
std::vector<std::byte> blob(sizeof(TrackMusicNNEmbeddings) - 1, std::byte{});
|
||||||
|
TrackMusicNNEmbeddings embeddings{};
|
||||||
|
EXPECT_THROW(trackMusicNNEmbeddingsFromBlob(blob, embeddings), Exception);
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::tests
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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 <array>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "musicnn/MusicNNModel.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::musicnn::tests
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path getMusicNNModelPathFromEnv()
|
||||||
|
{
|
||||||
|
const char* p{ std::getenv("LMS_MUSICNN_MODEL") };
|
||||||
|
return p ? std::filesystem::path{ p } : std::filesystem::path{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<float, MusicNNModel::inputFrames * MusicNNModel::inputBands> makeRandomPatch()
|
||||||
|
{
|
||||||
|
std::minstd_rand rng{ 42 };
|
||||||
|
std::uniform_real_distribution<float> dist{ 0.F, 1.F };
|
||||||
|
|
||||||
|
std::array<float, MusicNNModel::inputFrames * MusicNNModel::inputBands> patch{};
|
||||||
|
|
||||||
|
for (float& v : patch)
|
||||||
|
v = dist(rng);
|
||||||
|
|
||||||
|
return patch;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(MusicNNModel, CanConstruct)
|
||||||
|
{
|
||||||
|
const std::filesystem::path path{ getMusicNNModelPathFromEnv() };
|
||||||
|
|
||||||
|
if (path.empty())
|
||||||
|
GTEST_SKIP() << "LMS_MUSICNN_MODEL not set";
|
||||||
|
|
||||||
|
EXPECT_NO_THROW({ const MusicNNModel model{ path }; });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MusicNNModel, CanForward)
|
||||||
|
{
|
||||||
|
const std::filesystem::path path{ getMusicNNModelPathFromEnv() };
|
||||||
|
|
||||||
|
if (path.empty())
|
||||||
|
GTEST_SKIP() << "LMS_MUSICNN_MODEL not set";
|
||||||
|
|
||||||
|
const MusicNNModel model{ path };
|
||||||
|
const auto patch{ makeRandomPatch() };
|
||||||
|
|
||||||
|
EXPECT_NO_THROW({ [[maybe_unused]] const auto output{ model.forward(patch) }; });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::audio::musicnn::tests
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <random>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "audio/IPcmDecoder.hpp"
|
||||||
|
#include "audio/PcmTypes.hpp"
|
||||||
|
|
||||||
|
#include "utils/PcmSpectralFrameDecoder.hpp"
|
||||||
|
|
||||||
|
namespace lms::audio::tests
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// A mock IPcmDecoder that emits samples 0, 1, 2, 3, ... (as float) up to totalSamples,
|
||||||
|
class SequencePcmDecoder : public IPcmDecoder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SequencePcmDecoder(std::size_t totalSampleCount)
|
||||||
|
: _totalSampleCount{ totalSampleCount }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const PcmParameters& getParameters() const override { return _params; }
|
||||||
|
|
||||||
|
std::size_t readSamples(std::span<WritableBuffer> outputChannelBuffers) override
|
||||||
|
{
|
||||||
|
assert(outputChannelBuffers.size() == 1); // only planar
|
||||||
|
if (_finished)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
auto& buf{ outputChannelBuffers[0] };
|
||||||
|
if (buf.size() == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
assert(buf.size() % sizeof(float) == 0);
|
||||||
|
|
||||||
|
// not always writing up to what is requested
|
||||||
|
std::uniform_int_distribution dist{ std::size_t{ 1 }, buf.size() / sizeof(float) };
|
||||||
|
std::size_t sampleCountToWrite{ dist(_randomEngine) };
|
||||||
|
if (_currentSampleIndex + sampleCountToWrite > _totalSampleCount)
|
||||||
|
{
|
||||||
|
sampleCountToWrite = _totalSampleCount - _currentSampleIndex;
|
||||||
|
_finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float* dest{ reinterpret_cast<float*>(buf.data()) };
|
||||||
|
for (std::size_t i{}; i < sampleCountToWrite; ++i)
|
||||||
|
*(dest++) = static_cast<float>(_currentSampleIndex++);
|
||||||
|
|
||||||
|
return sampleCountToWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool finished() const override { return _finished; }
|
||||||
|
|
||||||
|
std::chrono::milliseconds getEstimatedDuration() const override
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::duration<float>{ static_cast<float>(_totalSampleCount) / static_cast<float>(_params.sampleRate) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const PcmParameters _params{
|
||||||
|
.channelCount = 1,
|
||||||
|
.sampleRate = 16000,
|
||||||
|
.sampleType = PcmSampleType::Float32,
|
||||||
|
.byteOrder = std::endian::native,
|
||||||
|
.planar = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::minstd_rand _randomEngine{ 42 }; // fixed seed for reproducibility
|
||||||
|
std::size_t _totalSampleCount{};
|
||||||
|
std::size_t _currentSampleIndex{};
|
||||||
|
bool _finished{};
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T, std::size_t N>
|
||||||
|
void expectSpanEq(std::span<const T, N> actual, const std::array<T, N>& expected)
|
||||||
|
{
|
||||||
|
for (std::size_t i{}; i < N; ++i)
|
||||||
|
EXPECT_FLOAT_EQ(actual[i], expected[i]) << "index=" << i;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::size_t WindowSize{ 8 };
|
||||||
|
constexpr std::size_t HopSize{ 4 };
|
||||||
|
using FrameDecoder = PcmSpectralFrameDecoder<WindowSize, float>;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(SequencePcmDecoder, basic)
|
||||||
|
{
|
||||||
|
constexpr std::size_t decoderTotalSampleCount{ 32 };
|
||||||
|
SequencePcmDecoder decoder{ decoderTotalSampleCount };
|
||||||
|
|
||||||
|
std::size_t totalSampleReadCount{};
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
std::array<float, 16> buffer{};
|
||||||
|
std::array outputBuffers{ IPcmDecoder::WritableBuffer{ std::as_writable_bytes(std::span{ buffer }) } };
|
||||||
|
const std::size_t sampleReadCount{ decoder.readSamples(outputBuffers) };
|
||||||
|
if (sampleReadCount == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < sampleReadCount; ++i)
|
||||||
|
EXPECT_FLOAT_EQ(buffer[i], totalSampleReadCount + i);
|
||||||
|
|
||||||
|
totalSampleReadCount += sampleReadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(totalSampleReadCount, decoderTotalSampleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PcmSpectralFrameDecoder, firstFrameIsCenteredOnSample0)
|
||||||
|
{
|
||||||
|
FrameDecoder frameDecoder{ std::make_unique<SequencePcmDecoder>(32), HopSize };
|
||||||
|
using Frame = std::array<float, WindowSize>;
|
||||||
|
std::vector<Frame> frames;
|
||||||
|
|
||||||
|
EXPECT_EQ(frameDecoder.currentFrameIndex(), 0);
|
||||||
|
const std::size_t decoded{ frameDecoder.decodeFrames(1,
|
||||||
|
[&](const FrameDecoder::SpectralFrameView& frame) {
|
||||||
|
auto& newFrame{ frames.emplace_back() };
|
||||||
|
std::copy(std::cbegin(frame.rawSamples), std::cend(frame.rawSamples), std::begin(newFrame));
|
||||||
|
}) };
|
||||||
|
|
||||||
|
ASSERT_EQ(decoded, 1);
|
||||||
|
ASSERT_EQ(frames.size(), 1);
|
||||||
|
EXPECT_EQ(frameDecoder.currentFrameIndex(), 1);
|
||||||
|
expectSpanEq<float, WindowSize>(frames[0], { 0.F, 0.F, 0.F, 0.F, 0.F, 1.F, 2.F, 3.F });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PcmSpectralFrameDecoder, framesAdvanceByHopSize)
|
||||||
|
{
|
||||||
|
FrameDecoder frameDecoder{ std::make_unique<SequencePcmDecoder>(16), HopSize };
|
||||||
|
using Frame = std::array<float, WindowSize>;
|
||||||
|
std::vector<Frame> frames;
|
||||||
|
|
||||||
|
const std::size_t decoded{ frameDecoder.decodeFrames(2,
|
||||||
|
[&](const FrameDecoder::SpectralFrameView& frame) {
|
||||||
|
auto& newFrame{ frames.emplace_back() };
|
||||||
|
std::copy(std::cbegin(frame.rawSamples), std::cend(frame.rawSamples), std::begin(newFrame));
|
||||||
|
}) };
|
||||||
|
|
||||||
|
ASSERT_EQ(decoded, 2);
|
||||||
|
ASSERT_EQ(frames.size(), 2);
|
||||||
|
EXPECT_EQ(frameDecoder.currentFrameIndex(), 2);
|
||||||
|
|
||||||
|
expectSpanEq<float, WindowSize>(
|
||||||
|
frames[0],
|
||||||
|
{ 0.F, 0.F, 0.F, 0.F, 0.F, 1.F, 2.F, 3.F });
|
||||||
|
|
||||||
|
expectSpanEq<float, WindowSize>(
|
||||||
|
frames[1],
|
||||||
|
{ 0.F, 1.F, 2.F, 3.F, 4.F, 5.F, 6.F, 7.F });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PcmSpectralFrameDecoder, skipFramesAdvancesState)
|
||||||
|
{
|
||||||
|
FrameDecoder frameDecoder{ std::make_unique<SequencePcmDecoder>(32), HopSize };
|
||||||
|
using Frame = std::array<float, WindowSize>;
|
||||||
|
std::vector<Frame> frames;
|
||||||
|
|
||||||
|
EXPECT_EQ(frameDecoder.currentFrameIndex(), 0);
|
||||||
|
ASSERT_EQ(frameDecoder.skipFrames(2), 2);
|
||||||
|
EXPECT_EQ(frameDecoder.currentFrameIndex(), 2);
|
||||||
|
|
||||||
|
Frame lastFrame{};
|
||||||
|
|
||||||
|
ASSERT_EQ(frameDecoder.decodeFrames(1,
|
||||||
|
[&](const FrameDecoder::SpectralFrameView& frame) {
|
||||||
|
std::copy(std::cbegin(frame.rawSamples), std::cend(frame.rawSamples), std::begin(lastFrame));
|
||||||
|
}),
|
||||||
|
1);
|
||||||
|
|
||||||
|
expectSpanEq<float, WindowSize>(
|
||||||
|
lastFrame,
|
||||||
|
{ 4.F, 5.F, 6.F, 7.F, 8.F, 9.F, 10.F, 11.F });
|
||||||
|
|
||||||
|
EXPECT_EQ(frameDecoder.currentFrameIndex(), 3);
|
||||||
|
}
|
||||||
|
} // namespace lms::audio::tests
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
|
||||||
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
|
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
|
||||||
|
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||||
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
|
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
|
||||||
|
|
||||||
set(LMS_VERSION ${PROJECT_VERSION})
|
set(LMS_VERSION ${PROJECT_VERSION})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
add_executable(bench-core
|
add_executable(bench-core
|
||||||
|
Core.cpp
|
||||||
TraceLoggerBench.cpp
|
TraceLoggerBench.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2025 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -17,10 +17,6 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
BENCHMARK_MAIN();
|
||||||
{
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
|
||||||
return RUN_ALL_TESTS();
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core::benchs
|
||||||
{
|
{
|
||||||
// The trace logger is meant to built/destroyed once
|
// The trace logger is meant to built/destroyed once
|
||||||
const Service<logging::ILogger> logger{ logging::createLogger() };
|
const Service<logging::ILogger> logger{ logging::createLogger() };
|
||||||
@@ -73,7 +73,4 @@ namespace lms::core
|
|||||||
BENCHMARK(BM_TraceLogger_Overview_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency());
|
BENCHMARK(BM_TraceLogger_Overview_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency());
|
||||||
BENCHMARK(BM_TraceLogger_Detailed)->Threads(1)->Threads(std::thread::hardware_concurrency());
|
BENCHMARK(BM_TraceLogger_Detailed)->Threads(1)->Threads(std::thread::hardware_concurrency());
|
||||||
BENCHMARK(BM_TraceLogger_Detailed_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency());
|
BENCHMARK(BM_TraceLogger_Detailed_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency());
|
||||||
|
} // namespace lms::core::benchs
|
||||||
} // namespace lms::core
|
|
||||||
|
|
||||||
BENCHMARK_MAIN();
|
|
||||||
@@ -18,14 +18,38 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "core/XxHash3.hpp"
|
#include "core/XxHash3.hpp"
|
||||||
|
#include "core/Exception.hpp"
|
||||||
|
|
||||||
#define XXH_INLINE_ALL
|
#define XXH_INLINE_ALL
|
||||||
#include <xxhash.h>
|
#include <xxhash.h>
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
std::uint64_t xxHash3_64(std::span<const std::byte> buf)
|
std::uint64_t XxHash3_64::hash(std::span<const std::byte> buf)
|
||||||
{
|
{
|
||||||
return XXH3_64bits(buf.data(), buf.size());
|
return XXH3_64bits(buf.data(), buf.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XxHash3_64::XxHash3_64()
|
||||||
|
: _state{ XXH3_createState() }
|
||||||
|
{
|
||||||
|
if (!_state)
|
||||||
|
throw LmsException{ "XXH3_createState failed: out of memory" };
|
||||||
|
XXH3_64bits_reset(static_cast<XXH3_state_t*>(_state));
|
||||||
|
}
|
||||||
|
|
||||||
|
XxHash3_64::~XxHash3_64()
|
||||||
|
{
|
||||||
|
XXH3_freeState(static_cast<XXH3_state_t*>(_state));
|
||||||
|
}
|
||||||
|
|
||||||
|
void XxHash3_64::update(std::span<const std::byte> buf)
|
||||||
|
{
|
||||||
|
XXH3_64bits_update(static_cast<XXH3_state_t*>(_state), buf.data(), buf.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t XxHash3_64::digest() const
|
||||||
|
{
|
||||||
|
return XXH3_64bits_digest(static_cast<const XXH3_state_t*>(_state));
|
||||||
|
}
|
||||||
} // namespace lms::core
|
} // namespace lms::core
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cstddef>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "core/Exception.hpp"
|
||||||
|
|
||||||
|
namespace lms::core
|
||||||
|
{
|
||||||
|
template<typename T, std::size_t Alignment>
|
||||||
|
class AlignedHeapArray
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using iterator = T*;
|
||||||
|
using const_iterator = const T*;
|
||||||
|
|
||||||
|
explicit AlignedHeapArray(std::size_t count)
|
||||||
|
: _count{ count }
|
||||||
|
, _values{ static_cast<T*>(std::aligned_alloc(Alignment, getStorageSize(count))) }
|
||||||
|
{
|
||||||
|
if (!_values)
|
||||||
|
throw LmsException{ "Allocation failed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
~AlignedHeapArray()
|
||||||
|
{
|
||||||
|
std::free(_values);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlignedHeapArray(const AlignedHeapArray&) = delete;
|
||||||
|
AlignedHeapArray& operator=(const AlignedHeapArray&) = delete;
|
||||||
|
|
||||||
|
std::size_t size() const { return _count; }
|
||||||
|
T* data() const { return _values; }
|
||||||
|
T& operator[](std::size_t index) const { return _values[index]; }
|
||||||
|
|
||||||
|
iterator begin() const { return _values; }
|
||||||
|
iterator end() const { return _values + _count; }
|
||||||
|
|
||||||
|
const_iterator cbegin() const { return _values; }
|
||||||
|
const_iterator cend() const { return _values + _count; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::size_t getStorageSize(std::size_t count)
|
||||||
|
{
|
||||||
|
const std::size_t bytes{ count * sizeof(T) };
|
||||||
|
const std::size_t alignedBytes{ ((bytes + Alignment - 1) / Alignment) * Alignment };
|
||||||
|
return alignedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t _count;
|
||||||
|
T* _values;
|
||||||
|
};
|
||||||
|
} // namespace lms::core
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include <boost/crc.hpp> // for boost::crc_32_type
|
#include <boost/crc.hpp> // for boost::crc_32_type
|
||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
namespace lms::core::random
|
namespace lms::core::random
|
||||||
{
|
{
|
||||||
@@ -43,12 +44,36 @@ namespace lms::core::random
|
|||||||
return dist(getRandGenerator());
|
return dist(getRandGenerator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename RandomEngine, typename Container>
|
||||||
|
requires std::is_floating_point_v<typename Container::value_type>
|
||||||
|
void fillContainer(RandomEngine& randomEngine, Container& container, typename Container::value_type min, typename Container::value_type max)
|
||||||
|
{
|
||||||
|
std::uniform_real_distribution<typename Container::value_type> distrib{ min, max };
|
||||||
|
for (auto& v : container)
|
||||||
|
v = distrib(randomEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename RandomEngine, typename Container>
|
||||||
|
requires std::is_integral_v<typename Container::value_type>
|
||||||
|
void fillContainer(RandomEngine& randomEngine, Container& container, typename Container::value_type min, typename Container::value_type max)
|
||||||
|
{
|
||||||
|
std::uniform_int_distribution<typename Container::value_type> distrib{ min, max };
|
||||||
|
for (auto& v : container)
|
||||||
|
v = distrib(randomEngine);
|
||||||
|
}
|
||||||
|
|
||||||
template<typename Container>
|
template<typename Container>
|
||||||
void shuffleContainer(Container& container)
|
void shuffleContainer(Container& container)
|
||||||
{
|
{
|
||||||
std::shuffle(std::begin(container), std::end(container), getRandGenerator());
|
std::shuffle(std::begin(container), std::end(container), getRandGenerator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename RandomEngine, typename Container>
|
||||||
|
void shuffleContainer(RandomEngine& randomEngine, Container& container)
|
||||||
|
{
|
||||||
|
std::shuffle(std::begin(container), std::end(container), randomEngine);
|
||||||
|
}
|
||||||
|
|
||||||
template<typename Container>
|
template<typename Container>
|
||||||
typename Container::const_iterator pickRandom(const Container& container)
|
typename Container::const_iterator pickRandom(const Container& container)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,18 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
namespace lms::core::utils
|
namespace lms::core::utils
|
||||||
{
|
{
|
||||||
template<typename Container, typename T>
|
|
||||||
void push_back_if_not_present(Container& container, const T& val)
|
|
||||||
{
|
|
||||||
if (std::find(std::cbegin(container), std::cend(container), val) == std::cend(container))
|
|
||||||
container.push_back(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<class... Ts>
|
template<class... Ts>
|
||||||
struct overloads : Ts...
|
struct overloads : Ts...
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,5 +25,20 @@
|
|||||||
|
|
||||||
namespace lms::core
|
namespace lms::core
|
||||||
{
|
{
|
||||||
std::uint64_t xxHash3_64(std::span<const std::byte> buf);
|
class XxHash3_64
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static std::uint64_t hash(std::span<const std::byte> buf);
|
||||||
|
|
||||||
|
XxHash3_64();
|
||||||
|
~XxHash3_64();
|
||||||
|
XxHash3_64(const XxHash3_64&) = delete;
|
||||||
|
XxHash3_64& operator=(const XxHash3_64&) = delete;
|
||||||
|
|
||||||
|
void update(std::span<const std::byte> buf);
|
||||||
|
std::uint64_t digest() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void* _state{};
|
||||||
|
};
|
||||||
} // namespace lms::core
|
} // namespace lms::core
|
||||||
@@ -10,15 +10,16 @@ add_executable(test-core
|
|||||||
Service.cpp
|
Service.cpp
|
||||||
String.cpp
|
String.cpp
|
||||||
TraceLogger.cpp
|
TraceLogger.cpp
|
||||||
Utils.cpp
|
|
||||||
UUID.cpp
|
UUID.cpp
|
||||||
XxHash3.cpp
|
XxHash3.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(test-core PRIVATE
|
target_link_libraries(test-core PRIVATE
|
||||||
lmscore
|
lmscore
|
||||||
|
lmsmath
|
||||||
Threads::Threads
|
Threads::Threads
|
||||||
GTest::GTest
|
GTest::GTest
|
||||||
|
GTest::gtest_main
|
||||||
)
|
)
|
||||||
|
|
||||||
if (NOT CMAKE_CROSSCOMPILING)
|
if (NOT CMAKE_CROSSCOMPILING)
|
||||||
|
|||||||
@@ -31,7 +31,27 @@ namespace lms::core
|
|||||||
for (std::size_t i{}; i < buffer.size(); ++i)
|
for (std::size_t i{}; i < buffer.size(); ++i)
|
||||||
buffer[i] = static_cast<std::byte>(i);
|
buffer[i] = static_cast<std::byte>(i);
|
||||||
|
|
||||||
const std::uint64_t hash{ xxHash3_64(buffer) };
|
const std::uint64_t hash{ XxHash3_64::hash(buffer) };
|
||||||
EXPECT_EQ(hash, 12137474952470826274ULL);
|
EXPECT_EQ(hash, 12137474952470826274ULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Xxhash3_64, streamingMatchesOneShot)
|
||||||
|
{
|
||||||
|
std::vector<std::byte> buffer;
|
||||||
|
buffer.resize(1024);
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < buffer.size(); ++i)
|
||||||
|
buffer[i] = static_cast<std::byte>(i);
|
||||||
|
|
||||||
|
const std::uint64_t expected{ XxHash3_64::hash(buffer) };
|
||||||
|
|
||||||
|
// Feed the same data in three unequal chunks to exercise the streaming path
|
||||||
|
constexpr std::size_t chunk1{ 100 };
|
||||||
|
constexpr std::size_t chunk2{ 400 };
|
||||||
|
XxHash3_64 hasher;
|
||||||
|
hasher.update(std::span{ buffer }.subspan(0, chunk1));
|
||||||
|
hasher.update(std::span{ buffer }.subspan(chunk1, chunk2));
|
||||||
|
hasher.update(std::span{ buffer }.subspan(chunk1 + chunk2));
|
||||||
|
EXPECT_EQ(hasher.digest(), expected);
|
||||||
|
}
|
||||||
} // namespace lms::core
|
} // namespace lms::core
|
||||||
@@ -28,7 +28,7 @@ add_library(lmsdatabase STATIC
|
|||||||
impl/objects/TrackBookmark.cpp
|
impl/objects/TrackBookmark.cpp
|
||||||
impl/objects/TrackEmbeddedImage.cpp
|
impl/objects/TrackEmbeddedImage.cpp
|
||||||
impl/objects/TrackEmbeddedImageLink.cpp
|
impl/objects/TrackEmbeddedImageLink.cpp
|
||||||
impl/objects/TrackFeatures.cpp
|
impl/objects/TrackMusicNNEmbeddings.cpp
|
||||||
impl/objects/TrackList.cpp
|
impl/objects/TrackList.cpp
|
||||||
impl/objects/TrackLyrics.cpp
|
impl/objects/TrackLyrics.cpp
|
||||||
impl/objects/Types.cpp
|
impl/objects/Types.cpp
|
||||||
@@ -38,7 +38,7 @@ add_library(lmsdatabase STATIC
|
|||||||
impl/IdType.cpp
|
impl/IdType.cpp
|
||||||
impl/Migration.cpp
|
impl/Migration.cpp
|
||||||
impl/Object.cpp
|
impl/Object.cpp
|
||||||
impl/QueryPlanRecorder.cpp
|
impl/profiling/QueryProfiler.cpp
|
||||||
impl/Session.cpp
|
impl/Session.cpp
|
||||||
impl/SqlQuery.cpp
|
impl/SqlQuery.cpp
|
||||||
impl/Transaction.cpp
|
impl/Transaction.cpp
|
||||||
@@ -59,6 +59,7 @@ target_include_directories(lmsdatabase PRIVATE
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lmsdatabase PRIVATE
|
target_link_libraries(lmsdatabase PRIVATE
|
||||||
|
lmsmath
|
||||||
Wt::DboSqlite3
|
Wt::DboSqlite3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 103 };
|
static constexpr Version LMS_DATABASE_VERSION{ 104 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -1706,6 +1706,23 @@ FROM track)");
|
|||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV103(Session& session)
|
||||||
|
{
|
||||||
|
// Drop previous track_audio_features with a brand new table dedicated to embeddings
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(DROP TABLE track_features)");
|
||||||
|
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_musicnn_embeddings" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"data" blob not null,
|
||||||
|
"track_id" bigint,
|
||||||
|
constraint "fk_track_musicnn_embeddings_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings RENAME COLUMN similarity_engine_type TO recommendation_engine_type");
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN musicnn_model_identifier TEXT NOT NULL DEFAULT ''");
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -1785,6 +1802,7 @@ FROM track)");
|
|||||||
{ 100, migrateFromV100 },
|
{ 100, migrateFromV100 },
|
||||||
{ 101, migrateFromV101 },
|
{ 101, migrateFromV101 },
|
||||||
{ 102, migrateFromV102 },
|
{ 102, migrateFromV102 },
|
||||||
|
{ 103, migrateFromV103 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -52,9 +52,9 @@
|
|||||||
#include "database/objects/TrackBookmark.hpp"
|
#include "database/objects/TrackBookmark.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackFeatures.hpp"
|
|
||||||
#include "database/objects/TrackList.hpp"
|
#include "database/objects/TrackList.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||||
#include "database/objects/UIState.hpp"
|
#include "database/objects/UIState.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ namespace lms::db
|
|||||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||||
_session.mapClass<TrackEmbeddedImage>("track_embedded_image");
|
_session.mapClass<TrackEmbeddedImage>("track_embedded_image");
|
||||||
_session.mapClass<TrackEmbeddedImageLink>("track_embedded_image_link");
|
_session.mapClass<TrackEmbeddedImageLink>("track_embedded_image_link");
|
||||||
_session.mapClass<TrackFeatures>("track_features");
|
_session.mapClass<TrackMusicNNEmbeddings>("track_musicnn_embeddings");
|
||||||
_session.mapClass<TrackList>("tracklist");
|
_session.mapClass<TrackList>("tracklist");
|
||||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||||
_session.mapClass<TrackLyrics>("track_lyrics");
|
_session.mapClass<TrackLyrics>("track_lyrics");
|
||||||
@@ -302,7 +302,7 @@ namespace lms::db
|
|||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id, type)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id, type)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_musicnn_embeddings_track_idx ON track_musicnn_embeddings(track_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)");
|
||||||
|
|||||||
@@ -29,10 +29,9 @@
|
|||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "core/Service.hpp"
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
#include "QueryPlanRecorder.hpp"
|
#include "profiling/ScopedQueryProfiler.hpp"
|
||||||
|
|
||||||
namespace lms::db::utils
|
namespace lms::db::utils
|
||||||
{
|
{
|
||||||
@@ -42,16 +41,6 @@ namespace lms::db::utils
|
|||||||
|
|
||||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
namespace detail
|
|
||||||
{
|
|
||||||
template<typename Query>
|
|
||||||
void recordQueryPlanIfNeeded(const Query& query)
|
|
||||||
{
|
|
||||||
if (IQueryPlanRecorder * recorder{ core::Service<IQueryPlanRecorder>::get() })
|
|
||||||
static_cast<QueryPlanRecorder*>(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString());
|
|
||||||
}
|
|
||||||
} // namespace detail
|
|
||||||
|
|
||||||
template<typename Query>
|
template<typename Query>
|
||||||
void applyRange(Query& query, std::optional<Range> range)
|
void applyRange(Query& query, std::optional<Range> range)
|
||||||
{
|
{
|
||||||
@@ -100,20 +89,22 @@ namespace lms::db::utils
|
|||||||
template<typename Query, typename UnaryFunc>
|
template<typename Query, typename UnaryFunc>
|
||||||
void forEachQueryResult(const Query& query, UnaryFunc&& func)
|
void forEachQueryResult(const Query& query, UnaryFunc&& func)
|
||||||
{
|
{
|
||||||
detail::recordQueryPlanIfNeeded(query);
|
|
||||||
|
|
||||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString());
|
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString());
|
||||||
|
|
||||||
forEachResult(query.resultList(), std::forward<UnaryFunc>(func));
|
ScopedQueryProfiler queryProfiler{ query };
|
||||||
|
forEachResult(query.resultList(), [&](const auto& result) {
|
||||||
|
queryProfiler.suspend();
|
||||||
|
func(result);
|
||||||
|
queryProfiler.resume();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T, typename Query>
|
template<typename T, typename Query>
|
||||||
std::vector<T> fetchQueryResults(const Query& query)
|
std::vector<T> fetchQueryResults(const Query& query)
|
||||||
{
|
{
|
||||||
detail::recordQueryPlanIfNeeded(query);
|
|
||||||
|
|
||||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
||||||
|
|
||||||
|
ScopedQueryProfiler queryProfiler{ query };
|
||||||
auto collection{ query.resultList() };
|
auto collection{ query.resultList() };
|
||||||
return std::vector<T>(collection.begin(), collection.end());
|
return std::vector<T>(collection.begin(), collection.end());
|
||||||
}
|
}
|
||||||
@@ -121,10 +112,9 @@ namespace lms::db::utils
|
|||||||
template<typename Query>
|
template<typename Query>
|
||||||
std::vector<typename QueryResultType<Query>::type> fetchQueryResults(const Query& query)
|
std::vector<typename QueryResultType<Query>::type> fetchQueryResults(const Query& query)
|
||||||
{
|
{
|
||||||
detail::recordQueryPlanIfNeeded(query);
|
|
||||||
|
|
||||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
||||||
|
|
||||||
|
ScopedQueryProfiler queryProfiler{ query };
|
||||||
auto collection{ query.resultList() };
|
auto collection{ query.resultList() };
|
||||||
return std::vector<typename QueryResultType<Query>::type>(collection.begin(), collection.end());
|
return std::vector<typename QueryResultType<Query>::type>(collection.begin(), collection.end());
|
||||||
}
|
}
|
||||||
@@ -132,9 +122,8 @@ namespace lms::db::utils
|
|||||||
template<typename Query>
|
template<typename Query>
|
||||||
auto fetchQuerySingleResult(const Query& query)
|
auto fetchQuerySingleResult(const Query& query)
|
||||||
{
|
{
|
||||||
detail::recordQueryPlanIfNeeded(query);
|
|
||||||
|
|
||||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString());
|
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString());
|
||||||
|
ScopedQueryProfiler queryProfiler{ query };
|
||||||
return query.resultValue();
|
return query.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +173,7 @@ namespace lms::db::utils
|
|||||||
moreResults = false;
|
moreResults = false;
|
||||||
|
|
||||||
std::size_t count{};
|
std::size_t count{};
|
||||||
|
ScopedQueryProfiler queryProfiler{ query };
|
||||||
const auto collection{ query.resultList() };
|
const auto collection{ query.resultList() };
|
||||||
auto it{ fetchFirstResult(collection) };
|
auto it{ fetchFirstResult(collection) };
|
||||||
while (it != collection.end())
|
while (it != collection.end())
|
||||||
@@ -194,7 +184,9 @@ namespace lms::db::utils
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryProfiler.suspend();
|
||||||
func(*it);
|
func(*it);
|
||||||
|
queryProfiler.resume();
|
||||||
fetchNextResult<ResultType>(it);
|
fetchNextResult<ResultType>(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,48 +403,6 @@ AND NOT EXISTS (
|
|||||||
return _preferredArtwork.id();
|
return _preferredArtwork.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
|
||||||
{
|
|
||||||
assert(session());
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "SELECT a.id FROM artist a"
|
|
||||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
|
||||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
|
||||||
" WHERE "
|
|
||||||
" t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c"
|
|
||||||
" INNER JOIN track t ON c.id = t_c.cluster_id"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
|
||||||
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
|
||||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
|
||||||
" WHERE a.id = ?)"
|
|
||||||
" AND a.id <> ?";
|
|
||||||
|
|
||||||
if (!artistLinkTypes.empty())
|
|
||||||
{
|
|
||||||
oss << " AND t_a_l.type IN (";
|
|
||||||
|
|
||||||
bool first{ true };
|
|
||||||
for (TrackArtistLinkType type : artistLinkTypes)
|
|
||||||
{
|
|
||||||
(void)type;
|
|
||||||
if (!first)
|
|
||||||
oss << ", ";
|
|
||||||
oss << "?";
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
oss << ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
auto query{ session()->query<ArtistId>(oss.str()).bind(getId()).bind(getId()).groupBy("a.id").orderBy("COUNT(*) DESC, RANDOM()") };
|
|
||||||
|
|
||||||
for (const TrackArtistLinkType type : artistLinkTypes)
|
|
||||||
query.bind(type);
|
|
||||||
|
|
||||||
return utils::execRangeQuery<ArtistId>(query, range);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const
|
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|||||||
@@ -749,33 +749,6 @@ namespace lms::db
|
|||||||
return utils::fetchQueryResults(query);
|
return utils::fetchQueryResults(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
|
||||||
{
|
|
||||||
assert(session());
|
|
||||||
|
|
||||||
// Select the similar releases using the 5 most used clusters of the release
|
|
||||||
auto query{ session()->query<Wt::Dbo::ptr<Release>>(
|
|
||||||
"SELECT r FROM release r"
|
|
||||||
" INNER JOIN track t ON t.release_id = r.id"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
|
||||||
" WHERE "
|
|
||||||
" t_c.cluster_id IN "
|
|
||||||
"(SELECT DISTINCT c.id FROM cluster c"
|
|
||||||
" INNER JOIN track t ON c.id = t_c.cluster_id"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
|
||||||
" INNER JOIN release r ON r.id = t.release_id"
|
|
||||||
" WHERE r.id = ?)"
|
|
||||||
" AND r.id <> ?")
|
|
||||||
.bind(getId())
|
|
||||||
.bind(getId())
|
|
||||||
.groupBy("r.id")
|
|
||||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
|
||||||
.limit(count ? static_cast<int>(*count) : -1)
|
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1) };
|
|
||||||
|
|
||||||
return utils::fetchQueryResults<Release::pointer>(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectPtr<Artwork> Release::getPreferredArtwork() const
|
ObjectPtr<Artwork> Release::getPreferredArtwork() const
|
||||||
{
|
{
|
||||||
return ObjectPtr<Artwork>{ _preferredArtwork };
|
return ObjectPtr<Artwork>{ _preferredArtwork };
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackFeatures.hpp"
|
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
|
||||||
@@ -191,6 +190,20 @@ namespace lms::db
|
|||||||
if (params.fileSize.has_value())
|
if (params.fileSize.has_value())
|
||||||
query.where("t.file_size = ?").bind(static_cast<long long>(params.fileSize.value()));
|
query.where("t.file_size = ?").bind(static_cast<long long>(params.fileSize.value()));
|
||||||
|
|
||||||
|
if (params.hasMusicNNEmbeddings.has_value())
|
||||||
|
{
|
||||||
|
if (*params.hasMusicNNEmbeddings)
|
||||||
|
query.where("EXISTS (SELECT t_m_e.track_id FROM track_musicnn_embeddings t_m_e WHERE t_m_e.track_id = t.id)");
|
||||||
|
else
|
||||||
|
query.where("NOT EXISTS (SELECT t_m_e.track_id FROM track_musicnn_embeddings t_m_e WHERE t_m_e.track_id = t.id)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.lastTrackId.isValid())
|
||||||
|
{
|
||||||
|
assert(params.sortMethod == TrackSortMethod::Id);
|
||||||
|
query.where("t.id > ?").bind(params.lastTrackId);
|
||||||
|
}
|
||||||
|
|
||||||
if (params.embeddedImageId.isValid())
|
if (params.embeddedImageId.isValid())
|
||||||
{
|
{
|
||||||
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_id = t.id");
|
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_id = t.id");
|
||||||
@@ -322,7 +335,7 @@ namespace lms::db
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Track::findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(TrackId trackId, const std::filesystem::path& absoluteFilePath)>& func)
|
void Track::findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const TrackLocationVisitor& func)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
|
|
||||||
@@ -334,6 +347,19 @@ namespace lms::db
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Track::findAbsoluteFilePath(Session& session, const FindParameters& params, const TrackLocationVisitor& func)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
std::string_view itemToSelect{ "t.id, t.absolute_file_path" };
|
||||||
|
|
||||||
|
auto query{ createQuery<std::tuple<TrackId, std::filesystem::path>>(session, itemToSelect, params) };
|
||||||
|
|
||||||
|
utils::forEachQueryRangeResult(query, params.range, [&](const auto& res) {
|
||||||
|
func(std::get<0>(res), std::get<1>(res));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void Track::find(Session& session, const IdRange<TrackId>& idRange, const std::function<void(const Track::pointer&)>& func)
|
void Track::find(Session& session, const IdRange<TrackId>& idRange, const std::function<void(const Track::pointer&)>& func)
|
||||||
{
|
{
|
||||||
assert(idRange.isValid());
|
assert(idRange.isValid());
|
||||||
@@ -385,15 +411,6 @@ namespace lms::db
|
|||||||
return utils::execRangeQuery<TrackId>(query, range);
|
return utils::execRangeQuery<TrackId>(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
|
|
||||||
{
|
|
||||||
session.checkReadTransaction();
|
|
||||||
|
|
||||||
auto query{ session.getDboSession()->query<TrackId>("SELECT t.id FROM track t").where("LENGTH(t.recording_mbid) > 0").where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") };
|
|
||||||
|
|
||||||
return utils::execRangeQuery<TrackId>(query, range);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
||||||
{
|
{
|
||||||
session.checkWriteTransaction();
|
session.checkWriteTransaction();
|
||||||
@@ -490,36 +507,11 @@ namespace lms::db
|
|||||||
utils::forEachQueryRangeResult(query, params.range, moreResults, func);
|
utils::forEachQueryRangeResult(query, params.range, moreResults, func);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
|
std::size_t Track::getCount(Session& session, const FindParameters& params)
|
||||||
{
|
{
|
||||||
assert(!tracks.empty());
|
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
|
|
||||||
std::ostringstream oss;
|
return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(*)", params));
|
||||||
for (std::size_t i{}; i < tracks.size(); ++i)
|
|
||||||
{
|
|
||||||
if (!oss.str().empty())
|
|
||||||
oss << ", ";
|
|
||||||
oss << "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
auto query{ session.getDboSession()->query<TrackId>(
|
|
||||||
"SELECT t.id FROM track t"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
|
||||||
" AND t_c.cluster_id IN (SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN ("
|
|
||||||
+ oss.str() + "))"
|
|
||||||
" AND t.id NOT IN ("
|
|
||||||
+ oss.str() + ")")
|
|
||||||
.groupBy("t.id")
|
|
||||||
.orderBy("COUNT(*) DESC, RANDOM()") };
|
|
||||||
|
|
||||||
for (TrackId trackId : tracks)
|
|
||||||
query.bind(trackId);
|
|
||||||
|
|
||||||
for (TrackId trackId : tracks)
|
|
||||||
query.bind(trackId);
|
|
||||||
|
|
||||||
return utils::execRangeQuery<TrackId>(query, range);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Track::setAbsoluteFilePath(const std::filesystem::path& filePath)
|
void Track::setAbsoluteFilePath(const std::filesystem::path& filePath)
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 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 "database/objects/TrackFeatures.hpp"
|
|
||||||
|
|
||||||
#include <Wt/Dbo/Impl.h>
|
|
||||||
#include <boost/property_tree/json_parser.hpp>
|
|
||||||
#include <boost/property_tree/ptree.hpp>
|
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "database/Session.hpp"
|
|
||||||
#include "database/objects/Directory.hpp"
|
|
||||||
#include "database/objects/Track.hpp"
|
|
||||||
|
|
||||||
#include "Utils.hpp"
|
|
||||||
#include "traits/IdTypeTraits.hpp"
|
|
||||||
|
|
||||||
DBO_INSTANTIATE_TEMPLATES(lms::db::TrackFeatures)
|
|
||||||
|
|
||||||
namespace lms::db
|
|
||||||
{
|
|
||||||
|
|
||||||
TrackFeatures::TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
|
||||||
: _data{ jsonEncodedFeatures }
|
|
||||||
, _track{ getDboPtr(track) }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
|
||||||
{
|
|
||||||
return session.getDboSession()->add(std::unique_ptr<TrackFeatures>{ new TrackFeatures{ track, jsonEncodedFeatures } });
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t TrackFeatures::getCount(Session& session)
|
|
||||||
{
|
|
||||||
session.checkReadTransaction();
|
|
||||||
|
|
||||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_features"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id)
|
|
||||||
{
|
|
||||||
session.checkReadTransaction();
|
|
||||||
|
|
||||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>().where("id = ?").bind(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
|
|
||||||
{
|
|
||||||
session.checkReadTransaction();
|
|
||||||
|
|
||||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>().where("track_id = ?").bind(trackId));
|
|
||||||
}
|
|
||||||
|
|
||||||
RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
|
|
||||||
{
|
|
||||||
session.checkReadTransaction();
|
|
||||||
|
|
||||||
auto query{ session.getDboSession()->query<TrackFeaturesId>("SELECT id from track_features") };
|
|
||||||
|
|
||||||
return utils::execRangeQuery<TrackFeaturesId>(query, range);
|
|
||||||
}
|
|
||||||
|
|
||||||
FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
|
|
||||||
{
|
|
||||||
FeatureValuesMap featuresValuesMap{ getFeatureValuesMap({ featureNode }) };
|
|
||||||
return std::move(featuresValuesMap[featureNode]);
|
|
||||||
}
|
|
||||||
|
|
||||||
FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
|
|
||||||
{
|
|
||||||
FeatureValuesMap res;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::istringstream iss{ _data };
|
|
||||||
boost::property_tree::ptree root;
|
|
||||||
|
|
||||||
boost::property_tree::read_json(iss, root);
|
|
||||||
|
|
||||||
for (const FeatureName& featureName : featureNames)
|
|
||||||
{
|
|
||||||
FeatureValues& featureValues{ res[featureName] };
|
|
||||||
|
|
||||||
auto node{ root.get_child(featureName) };
|
|
||||||
|
|
||||||
bool hasChildren = false;
|
|
||||||
for (const auto& child : node.get_child(""))
|
|
||||||
{
|
|
||||||
hasChildren = true;
|
|
||||||
featureValues.push_back(child.second.get_value<double>());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasChildren)
|
|
||||||
featureValues.push_back(node.get_value<double>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (boost::property_tree::ptree_error& error)
|
|
||||||
{
|
|
||||||
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
|
|
||||||
res.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace lms::db
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* 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 "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Impl.h>
|
||||||
|
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/objects/Track.hpp"
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|
||||||
|
DBO_INSTANTIATE_TEMPLATES(lms::db::TrackMusicNNEmbeddings)
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
TrackMusicNNEmbeddings::TrackMusicNNEmbeddings(ObjectPtr<Track> track)
|
||||||
|
: _track{ getDboPtr(track) }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackMusicNNEmbeddings::pointer TrackMusicNNEmbeddings::create(Session& session, ObjectPtr<Track> track)
|
||||||
|
{
|
||||||
|
return session.getDboSession()->add(std::unique_ptr<TrackMusicNNEmbeddings>{ new TrackMusicNNEmbeddings{ track } });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t TrackMusicNNEmbeddings::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_musicnn_embeddings"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackMusicNNEmbeddings::pointer TrackMusicNNEmbeddings::find(Session& session, TrackMusicNNEmbeddingsId id)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackMusicNNEmbeddings>().where("id = ?").bind(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackMusicNNEmbeddings::pointer TrackMusicNNEmbeddings::find(Session& session, TrackId trackId)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackMusicNNEmbeddings>().where("track_id = ?").bind(trackId));
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackMusicNNEmbeddingsId> TrackMusicNNEmbeddings::find(Session& session, std::optional<Range> range)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
auto query{ session.getDboSession()->query<TrackMusicNNEmbeddingsId>("SELECT id from track_musicnn_embeddings") };
|
||||||
|
|
||||||
|
return utils::execRangeQuery<TrackMusicNNEmbeddingsId>(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrackMusicNNEmbeddings::find(Session& session, std::function<void(const pointer&)> func)
|
||||||
|
{
|
||||||
|
auto query{ session.getDboSession()->find<TrackMusicNNEmbeddings>() };
|
||||||
|
|
||||||
|
utils::forEachQueryResult(query, [&](const TrackMusicNNEmbeddings::pointer& embeddings) {
|
||||||
|
func(embeddings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrackMusicNNEmbeddings::removeAll(Session& session)
|
||||||
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
utils::executeCommand(*session.getDboSession(), "DELETE FROM track_musicnn_embeddings");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::byte> TrackMusicNNEmbeddings::getData() const
|
||||||
|
{
|
||||||
|
return std::span<const std::byte>{ reinterpret_cast<const std::byte*>(_data.data()), _data.size() };
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrackMusicNNEmbeddings::setData(std::span<const std::byte> data)
|
||||||
|
{
|
||||||
|
const auto* start{ reinterpret_cast<const unsigned char*>(data.data()) };
|
||||||
|
_data.assign(start, start + data.size());
|
||||||
|
}
|
||||||
|
} // namespace lms::db
|
||||||
+37
-18
@@ -17,7 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "QueryPlanRecorder.hpp"
|
#include "profiling/QueryProfiler.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -29,35 +29,38 @@
|
|||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
std::unique_ptr<IQueryPlanRecorder> createQueryPlanRecorder()
|
std::unique_ptr<IQueryProfiler> createQueryProfiler()
|
||||||
{
|
{
|
||||||
return std::make_unique<QueryPlanRecorder>();
|
return std::make_unique<QueryProfiler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryPlanRecorder::QueryPlanRecorder()
|
QueryProfiler::QueryProfiler()
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO, "Recording database query plans");
|
LMS_LOG(DB, INFO, "Recording database queries");
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryPlanRecorder::~QueryPlanRecorder() = default;
|
QueryProfiler::~QueryProfiler() = default;
|
||||||
|
|
||||||
void QueryPlanRecorder::visitQueryPlans(const QueryPlanVisitor& visitor) const
|
void QueryProfiler::visitQueries(const QueryVisitor& visitor) const
|
||||||
{
|
{
|
||||||
const std::shared_lock lock{ _mutex };
|
const std::shared_lock lock{ _mutex };
|
||||||
|
|
||||||
for (const auto& [query, plan] : _queryPlans)
|
for (const auto& [query, data] : _queries)
|
||||||
visitor(query, plan);
|
{
|
||||||
|
const QueryStats stats{
|
||||||
|
.query = query,
|
||||||
|
.plan = data.plan,
|
||||||
|
.callCount = data.timeStats.getCount(),
|
||||||
|
.totalTime = std::chrono::microseconds{ static_cast<long long>(data.timeStats.getMean() * static_cast<double>(data.timeStats.getCount())) },
|
||||||
|
.meanTime = std::chrono::microseconds{ static_cast<long long>(data.timeStats.getMean()) },
|
||||||
|
.stdDevTime = std::chrono::microseconds{ static_cast<long long>(data.timeStats.getSampleStdDev()) },
|
||||||
|
};
|
||||||
|
visitor(stats);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QueryPlanRecorder::recordQueryPlanIfNeeded(Wt::Dbo::Session& session, const std::string& query)
|
void QueryProfiler::recordQueryPlan(Wt::Dbo::Session& session, const std::string& query)
|
||||||
{
|
{
|
||||||
{
|
|
||||||
const std::shared_lock lock{ _mutex };
|
|
||||||
|
|
||||||
if (_queryPlans.contains(query))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction{ session };
|
Wt::Dbo::Transaction transaction{ session };
|
||||||
|
|
||||||
Wt::Dbo::SqlConnection* connection{ transaction.connection() };
|
Wt::Dbo::SqlConnection* connection{ transaction.connection() };
|
||||||
@@ -106,7 +109,23 @@ namespace lms::db
|
|||||||
|
|
||||||
{
|
{
|
||||||
const std::unique_lock lock{ _mutex };
|
const std::unique_lock lock{ _mutex };
|
||||||
_queryPlans.try_emplace(query, std::move(result));
|
_queries[query].plan = std::move(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QueryProfiler::recordQueryExecution(Wt::Dbo::Session& session, const std::string& query, Clock::duration elapsed)
|
||||||
|
{
|
||||||
|
bool needQueryPlan{};
|
||||||
|
const double elapsedUs{ std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(elapsed).count() };
|
||||||
|
{
|
||||||
|
std::unique_lock lock{ _mutex };
|
||||||
|
|
||||||
|
auto& queryStats{ _queries[query] };
|
||||||
|
queryStats.timeStats.add(elapsedUs);
|
||||||
|
needQueryPlan = queryStats.plan.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needQueryPlan)
|
||||||
|
recordQueryPlan(session, query);
|
||||||
|
}
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
+18
-9
@@ -25,24 +25,33 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/Session.h>
|
#include <Wt/Dbo/Session.h>
|
||||||
|
|
||||||
#include "database/IQueryPlanRecorder.hpp"
|
#include "database/profiling/IQueryProfiler.hpp"
|
||||||
|
#include "math/StatsAccumulator.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class QueryPlanRecorder : public IQueryPlanRecorder
|
class QueryProfiler : public IQueryProfiler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
QueryPlanRecorder();
|
QueryProfiler();
|
||||||
~QueryPlanRecorder() override;
|
~QueryProfiler() override;
|
||||||
QueryPlanRecorder(const QueryPlanRecorder&) = delete;
|
QueryProfiler(const QueryProfiler&) = delete;
|
||||||
QueryPlanRecorder& operator=(const QueryPlanRecorder&) = delete;
|
QueryProfiler& operator=(const QueryProfiler&) = delete;
|
||||||
|
|
||||||
void visitQueryPlans(const QueryPlanVisitor& visitor) const override;
|
void visitQueries(const QueryVisitor& visitor) const override;
|
||||||
|
|
||||||
void recordQueryPlanIfNeeded(Wt::Dbo::Session& session, const std::string& query);
|
void recordQueryExecution(Wt::Dbo::Session& session, const std::string& query, Clock::duration elapsed);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void recordQueryPlan(Wt::Dbo::Session& session, const std::string& query);
|
||||||
|
|
||||||
|
struct QueryData
|
||||||
|
{
|
||||||
|
std::string plan;
|
||||||
|
math::StatsAccumulator<double> timeStats; // in Us
|
||||||
|
};
|
||||||
|
|
||||||
mutable std::shared_mutex _mutex;
|
mutable std::shared_mutex _mutex;
|
||||||
std::map<std::string, std::string> _queryPlans;
|
std::map<std::string, QueryData> _queries;
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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 <cassert>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/Service.hpp"
|
||||||
|
#include "database/profiling/IQueryProfiler.hpp"
|
||||||
|
|
||||||
|
#include "profiling/QueryProfiler.hpp"
|
||||||
|
|
||||||
|
namespace lms::db::utils
|
||||||
|
{
|
||||||
|
template<typename Query>
|
||||||
|
class ScopedQueryProfiler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ScopedQueryProfiler(const Query& query)
|
||||||
|
: _recorder{ static_cast<QueryProfiler*>(core::Service<IQueryProfiler>::get()) }
|
||||||
|
{
|
||||||
|
if (_recorder)
|
||||||
|
{
|
||||||
|
_query = &query;
|
||||||
|
_start = IQueryProfiler::Clock::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedQueryProfiler()
|
||||||
|
{
|
||||||
|
if (_recorder)
|
||||||
|
{
|
||||||
|
if (_active)
|
||||||
|
_elapsed += IQueryProfiler::Clock::now() - _start;
|
||||||
|
_recorder->recordQueryExecution(_query->session(), _query->asString(), _elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedQueryProfiler(const ScopedQueryProfiler&) = delete;
|
||||||
|
ScopedQueryProfiler& operator=(const ScopedQueryProfiler&) = delete;
|
||||||
|
|
||||||
|
void suspend()
|
||||||
|
{
|
||||||
|
if (_recorder)
|
||||||
|
{
|
||||||
|
assert(_active);
|
||||||
|
_elapsed += IQueryProfiler::Clock::now() - _start;
|
||||||
|
_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resume()
|
||||||
|
{
|
||||||
|
if (_recorder)
|
||||||
|
{
|
||||||
|
assert(!_active);
|
||||||
|
_start = IQueryProfiler::Clock::now();
|
||||||
|
_active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QueryProfiler* _recorder{};
|
||||||
|
const Query* _query{};
|
||||||
|
IQueryProfiler::Clock::time_point _start;
|
||||||
|
IQueryProfiler::Clock::duration _elapsed{};
|
||||||
|
bool _active{ true };
|
||||||
|
};
|
||||||
|
} // namespace lms::db::utils
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
#include <Wt/Dbo/collection.h>
|
#include <Wt/Dbo/collection.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "core/EnumSet.hpp"
|
|
||||||
#include "core/UUID.hpp"
|
#include "core/UUID.hpp"
|
||||||
|
|
||||||
#include "database/IdRange.hpp"
|
#include "database/IdRange.hpp"
|
||||||
@@ -39,7 +38,6 @@
|
|||||||
#include "database/objects/ArtworkId.hpp"
|
#include "database/objects/ArtworkId.hpp"
|
||||||
#include "database/objects/Filters.hpp"
|
#include "database/objects/Filters.hpp"
|
||||||
#include "database/objects/MediaLibraryId.hpp"
|
#include "database/objects/MediaLibraryId.hpp"
|
||||||
#include "database/objects/ReleaseId.hpp"
|
|
||||||
#include "database/objects/TrackId.hpp"
|
#include "database/objects/TrackId.hpp"
|
||||||
#include "database/objects/Types.hpp"
|
#include "database/objects/Types.hpp"
|
||||||
#include "database/objects/UserId.hpp"
|
#include "database/objects/UserId.hpp"
|
||||||
@@ -148,9 +146,6 @@ namespace lms::db
|
|||||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||||
ArtworkId getPreferredArtworkId() const;
|
ArtworkId getPreferredArtworkId() const;
|
||||||
|
|
||||||
// No artistLinkTypes means get them all
|
|
||||||
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
|
||||||
|
|
||||||
// Get the cluster of the tracks made by this artist
|
// Get the cluster of the tracks made by this artist
|
||||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||||
// size is the max number of cluster per cluster type
|
// size is the max number of cluster per cluster type
|
||||||
|
|||||||
@@ -349,8 +349,6 @@ namespace lms::db
|
|||||||
void visitTrackArtists(TrackArtistLinkType type, std::function<void(const ObjectPtr<Artist>&)> visitor) const;
|
void visitTrackArtists(TrackArtistLinkType type, std::function<void(const ObjectPtr<Artist>&)> visitor) const;
|
||||||
std::vector<ArtistId> getTrackArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
std::vector<ArtistId> getTrackArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||||
bool hasVariousArtists() const;
|
bool hasVariousArtists() const;
|
||||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ namespace lms::db
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Do not modify values (just add)
|
// Do not modify values (just add)
|
||||||
enum class SimilarityEngineType
|
enum class RecommendationEngineType
|
||||||
{
|
{
|
||||||
Clusters = 0,
|
Clusters = 0,
|
||||||
Features,
|
None = 2,
|
||||||
None,
|
AudioSimilarity = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
ScanSettings() = default;
|
ScanSettings() = default;
|
||||||
@@ -65,10 +65,11 @@ namespace lms::db
|
|||||||
// Getters
|
// Getters
|
||||||
std::size_t getAudioScanVersion() const { return _audioScanVersion; }
|
std::size_t getAudioScanVersion() const { return _audioScanVersion; }
|
||||||
std::size_t getArtistInfoScanVersion() const { return _artistInfoScanVersion; }
|
std::size_t getArtistInfoScanVersion() const { return _artistInfoScanVersion; }
|
||||||
|
std::string_view getMusicNNModelIdentifier() const { return _musicnnModelIdentifier; }
|
||||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||||
std::vector<std::string> getArtistTagDelimiters() const;
|
std::vector<std::string> getArtistTagDelimiters() const;
|
||||||
std::vector<std::string> getDefaultTagDelimiters() const;
|
std::vector<std::string> getDefaultTagDelimiters() const;
|
||||||
std::vector<std::string> getArtistsToNotSplit() const;
|
std::vector<std::string> getArtistsToNotSplit() const;
|
||||||
@@ -80,14 +81,14 @@ namespace lms::db
|
|||||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||||
void setExtraTagsToScan(std::span<const std::string_view> extraTags);
|
void setExtraTagsToScan(std::span<const std::string_view> extraTags);
|
||||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = type; }
|
||||||
void setArtistTagDelimiters(std::span<const std::string_view> delimiters);
|
void setArtistTagDelimiters(std::span<const std::string_view> delimiters);
|
||||||
void setArtistsToNotSplit(std::span<const std::string_view> artists);
|
void setArtistsToNotSplit(std::span<const std::string_view> artists);
|
||||||
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
|
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
|
||||||
void setSkipSingleReleasePlayLists(bool value);
|
void setSkipSingleReleasePlayLists(bool value);
|
||||||
void setAllowMBIDArtistMerge(bool value);
|
void setAllowMBIDArtistMerge(bool value);
|
||||||
void setArtistImageFallbackToReleaseField(bool value);
|
void setArtistImageFallbackToReleaseField(bool value);
|
||||||
|
void setMusicNNModelIdentifier(std::string_view identifier) { _musicnnModelIdentifier = identifier; }
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
@@ -96,7 +97,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _artistInfoScanVersion, "artist_info_scan_version");
|
Wt::Dbo::field(a, _artistInfoScanVersion, "artist_info_scan_version");
|
||||||
Wt::Dbo::field(a, _startTime, "start_time");
|
Wt::Dbo::field(a, _startTime, "start_time");
|
||||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
Wt::Dbo::field(a, _recommendationEngineType, "recommendation_engine_type");
|
||||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||||
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
||||||
Wt::Dbo::field(a, _artistsToNotSplit, "artists_to_not_split");
|
Wt::Dbo::field(a, _artistsToNotSplit, "artists_to_not_split");
|
||||||
@@ -104,6 +105,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
||||||
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
|
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
|
||||||
Wt::Dbo::field(a, _artistImageFallbackToReleaseField, "artist_image_fallback_to_release");
|
Wt::Dbo::field(a, _artistImageFallbackToReleaseField, "artist_image_fallback_to_release");
|
||||||
|
Wt::Dbo::field(a, _musicnnModelIdentifier, "musicnn_model_identifier");
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -119,7 +121,7 @@ namespace lms::db
|
|||||||
int _artistInfoScanVersion{};
|
int _artistInfoScanVersion{};
|
||||||
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
||||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
RecommendationEngineType _recommendationEngineType{ RecommendationEngineType::Clusters };
|
||||||
std::string _extraTagsToScan;
|
std::string _extraTagsToScan;
|
||||||
std::string _artistTagDelimiters;
|
std::string _artistTagDelimiters;
|
||||||
std::string _artistsToNotSplit;
|
std::string _artistsToNotSplit;
|
||||||
@@ -127,5 +129,6 @@ namespace lms::db
|
|||||||
bool _skipSingleReleasePlayLists{};
|
bool _skipSingleReleasePlayLists{};
|
||||||
bool _allowMBIDArtistMerge{};
|
bool _allowMBIDArtistMerge{};
|
||||||
bool _artistImageFallbackToReleaseField{};
|
bool _artistImageFallbackToReleaseField{};
|
||||||
|
std::string _musicnnModelIdentifier;
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ namespace lms::db
|
|||||||
DirectoryId directory; // if set, tracks in this directory
|
DirectoryId directory; // if set, tracks in this directory
|
||||||
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
|
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
|
||||||
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
|
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
|
||||||
|
std::optional<bool> hasMusicNNEmbeddings; // If set, tracks that have (or not) MusicNN embeddings
|
||||||
|
TrackId lastTrackId; // If set, tracks that are after this one, must be used with sort by id
|
||||||
|
|
||||||
FindParameters& setFilters(const Filters& _filters)
|
FindParameters& setFilters(const Filters& _filters)
|
||||||
{
|
{
|
||||||
@@ -191,6 +193,16 @@ namespace lms::db
|
|||||||
embeddedImageId = _embeddedImageId;
|
embeddedImageId = _embeddedImageId;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setHasMusicNNEmbeddings(std::optional<bool> _hasMusicNNEmbeddings)
|
||||||
|
{
|
||||||
|
hasMusicNNEmbeddings = _hasMusicNNEmbeddings;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
FindParameters& setLastTrackId(TrackId _lastTrackId)
|
||||||
|
{
|
||||||
|
lastTrackId = _lastTrackId;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Track() = default;
|
Track() = default;
|
||||||
@@ -203,19 +215,20 @@ namespace lms::db
|
|||||||
static void find(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library = {});
|
static void find(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library = {});
|
||||||
static void find(Session& session, const IdRange<TrackId>& idRange, const std::function<void(const Track::pointer&)>& func);
|
static void find(Session& session, const IdRange<TrackId>& idRange, const std::function<void(const Track::pointer&)>& func);
|
||||||
static IdRange<TrackId> findNextIdRange(Session& session, TrackId lastRetrievedId, std::size_t count);
|
static IdRange<TrackId> findNextIdRange(Session& session, TrackId lastRetrievedId, std::size_t count);
|
||||||
static void findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(TrackId trackId, const std::filesystem::path& absoluteFilePath)>& func);
|
|
||||||
|
using TrackLocationVisitor = std::function<void(TrackId trackId, const std::filesystem::path& absoluteFilePath)>;
|
||||||
|
static void findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const TrackLocationVisitor& func);
|
||||||
|
static void findAbsoluteFilePath(Session& session, const FindParameters& params, const TrackLocationVisitor& func);
|
||||||
|
|
||||||
static bool exists(Session& session, TrackId id);
|
static bool exists(Session& session, TrackId id);
|
||||||
static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID);
|
static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID);
|
||||||
static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID);
|
static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID);
|
||||||
static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, std::optional<Range> range = std::nullopt);
|
static RangeResults<TrackId> findIds(Session& session, const FindParameters& params);
|
||||||
|
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||||
static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
|
static void find(Session& session, const FindParameters& params, const std::function<void(const Track::pointer&)>& func);
|
||||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
static void find(Session& session, const FindParameters& params, bool& moreResults, const std::function<void(const Track::pointer&)>& func);
|
||||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Track::pointer&)>& func);
|
static std::size_t getCount(Session& session, const FindParameters& params);
|
||||||
static void find(Session& session, const FindParameters& parameters, bool& moreResults, const std::function<void(const Track::pointer&)>& func);
|
|
||||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
|
|
||||||
|
|
||||||
// Update utility functions
|
// Update utility functions
|
||||||
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||||
|
|||||||
+16
-21
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2018 Emeric Poupon
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -20,10 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <span>
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <Wt/Dbo/Field.h>
|
#include <Wt/Dbo/Field.h>
|
||||||
|
|
||||||
@@ -32,34 +29,33 @@
|
|||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "database/objects/TrackId.hpp"
|
#include "database/objects/TrackId.hpp"
|
||||||
|
|
||||||
LMS_DECLARE_IDTYPE(TrackFeaturesId)
|
LMS_DECLARE_IDTYPE(TrackMusicNNEmbeddingsId)
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Session;
|
class Session;
|
||||||
class Track;
|
class Track;
|
||||||
|
|
||||||
using FeatureName = std::string;
|
class TrackMusicNNEmbeddings final : public Object<TrackMusicNNEmbeddings, TrackMusicNNEmbeddingsId>
|
||||||
using FeatureValues = std::vector<double>;
|
|
||||||
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
|
|
||||||
|
|
||||||
class TrackFeatures final : public Object<TrackFeatures, TrackFeaturesId>
|
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TrackFeatures() = default;
|
TrackMusicNNEmbeddings() = default;
|
||||||
|
|
||||||
// Find utilities
|
// Find utilities
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, TrackFeaturesId id);
|
static pointer find(Session& session, TrackMusicNNEmbeddingsId id);
|
||||||
static pointer find(Session& session, TrackId trackId);
|
static pointer find(Session& session, TrackId trackId);
|
||||||
static RangeResults<TrackFeaturesId> find(Session& session, std::optional<Range> range = std::nullopt);
|
static RangeResults<TrackMusicNNEmbeddingsId> find(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
static void find(Session& session, std::function<void(const pointer&)> func);
|
||||||
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
static void removeAll(Session& session);
|
||||||
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
|
std::span<const std::byte> getData() const;
|
||||||
|
TrackId getTrackId() const { return _track.id(); }
|
||||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||||
|
|
||||||
|
void setData(std::span<const std::byte> data);
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
@@ -69,11 +65,10 @@ namespace lms::db
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
TrackMusicNNEmbeddings(ObjectPtr<Track> track);
|
||||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
static pointer create(Session& session, ObjectPtr<Track> track);
|
||||||
|
|
||||||
std::string _data;
|
std::vector<unsigned char> _data;
|
||||||
Wt::Dbo::ptr<Track> _track;
|
Wt::Dbo::ptr<Track> _track;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
+19
-5
@@ -19,21 +19,35 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
// Due to technical limitations, query plans are recorded globally across all databases.
|
// Due to technical limitations, query plans are recorded globally across all databases.
|
||||||
// As a result, this class is implemented as a singleton rather than being owned per DB instance.
|
// As a result, this class is implemented as a singleton rather than being owned per DB instance.
|
||||||
class IQueryPlanRecorder
|
class IQueryProfiler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~IQueryPlanRecorder() = default;
|
virtual ~IQueryProfiler() = default;
|
||||||
|
|
||||||
using QueryPlanVisitor = std::function<void(std::string_view query, std::string_view plan)>;
|
using Clock = std::chrono::steady_clock;
|
||||||
virtual void visitQueryPlans(const QueryPlanVisitor& visitor) const = 0;
|
|
||||||
|
struct QueryStats
|
||||||
|
{
|
||||||
|
std::string_view query;
|
||||||
|
std::string_view plan;
|
||||||
|
std::size_t callCount{};
|
||||||
|
std::chrono::microseconds totalTime{};
|
||||||
|
std::chrono::microseconds meanTime{};
|
||||||
|
std::chrono::microseconds stdDevTime{};
|
||||||
|
};
|
||||||
|
|
||||||
|
using QueryVisitor = std::function<void(const QueryStats&)>;
|
||||||
|
virtual void visitQueries(const QueryVisitor& visitor) const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IQueryPlanRecorder> createQueryPlanRecorder();
|
std::unique_ptr<IQueryProfiler> createQueryProfiler();
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
@@ -27,7 +27,6 @@ add_executable(test-database
|
|||||||
TrackArtistLink.cpp
|
TrackArtistLink.cpp
|
||||||
TrackBookmark.cpp
|
TrackBookmark.cpp
|
||||||
TrackEmbeddedImage.cpp
|
TrackEmbeddedImage.cpp
|
||||||
TrackFeatures.cpp
|
|
||||||
TrackList.cpp
|
TrackList.cpp
|
||||||
TrackLyrics.cpp
|
TrackLyrics.cpp
|
||||||
User.cpp
|
User.cpp
|
||||||
@@ -36,6 +35,7 @@ add_executable(test-database
|
|||||||
target_link_libraries(test-database PRIVATE
|
target_link_libraries(test-database PRIVATE
|
||||||
lmsdatabase
|
lmsdatabase
|
||||||
GTest::GTest
|
GTest::GTest
|
||||||
|
GTest::gtest_main
|
||||||
)
|
)
|
||||||
|
|
||||||
if (NOT CMAKE_CROSSCOMPILING)
|
if (NOT CMAKE_CROSSCOMPILING)
|
||||||
|
|||||||
@@ -568,81 +568,6 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
|
|
||||||
{
|
|
||||||
std::list<ScopedTrack> tracks;
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyClusterType" };
|
|
||||||
|
|
||||||
for (std::size_t i{}; i < 10; ++i)
|
|
||||||
{
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
cluster.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
const auto similarTracks{ Track::findSimilarTrackIds(session, { tracks.front().getId() }) };
|
|
||||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
|
||||||
for (const TrackId similarTrackId : similarTracks.results)
|
|
||||||
{
|
|
||||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
|
||||||
{
|
|
||||||
std::list<ScopedTrack> tracks;
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
|
||||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
|
||||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
|
||||||
|
|
||||||
for (std::size_t i{}; i < 5; ++i)
|
|
||||||
{
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (std::size_t i{ 5 }; i < 10; ++i)
|
|
||||||
{
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
|
||||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto similarTracks{ Track::findSimilarTrackIds(session, { tracks.back().getId() }, Range{ 0, 4 }) };
|
|
||||||
EXPECT_EQ(similarTracks.results.size(), 4);
|
|
||||||
for (const TrackId similarTrackId : similarTracks.results)
|
|
||||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto similarTracks{ Track::findSimilarTrackIds(session, { tracks.front().getId() }) };
|
|
||||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
|
||||||
for (const TrackId similarTrackId : similarTracks.results)
|
|
||||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||||
{
|
{
|
||||||
ScopedTrack track{ session };
|
ScopedTrack track{ session };
|
||||||
@@ -798,143 +723,4 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
|
||||||
{
|
|
||||||
ScopedArtist artist1{ session, "MyArtist1" };
|
|
||||||
ScopedArtist artist2{ session, "MyArtist2" };
|
|
||||||
ScopedArtist artist3{ session, "MyArtist3" };
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
|
||||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
|
||||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
EXPECT_EQ(artist1->findSimilarArtistIds().results.size(), 0);
|
|
||||||
EXPECT_EQ(artist2->findSimilarArtistIds().results.size(), 0);
|
|
||||||
EXPECT_EQ(artist3->findSimilarArtistIds().results.size(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::list<ScopedTrack> tracks;
|
|
||||||
for (std::size_t i{}; i < 10; ++i)
|
|
||||||
{
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
|
|
||||||
if (i < 5)
|
|
||||||
session.create<TrackArtistLink>(tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
session.create<TrackArtistLink>(tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist);
|
|
||||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
session.create<TrackArtistLink>(tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
|
|
||||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto artists{ artist1->findSimilarArtistIds() };
|
|
||||||
ASSERT_EQ(artists.results.size(), 1);
|
|
||||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist }) };
|
|
||||||
ASSERT_EQ(artists.results.size(), 1);
|
|
||||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Lyricist }) };
|
|
||||||
EXPECT_EQ(artists.results.size(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist, TrackArtistLinkType::Lyricist }) };
|
|
||||||
ASSERT_EQ(artists.results.size(), 1);
|
|
||||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Composer }) };
|
|
||||||
EXPECT_EQ(artists.results.size(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto artists{ artist2->findSimilarArtistIds() };
|
|
||||||
ASSERT_EQ(artists.results.size(), 2);
|
|
||||||
EXPECT_EQ(artists.results[0], artist1.getId());
|
|
||||||
EXPECT_EQ(artists.results[1], artist3.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
|
|
||||||
{
|
|
||||||
ScopedRelease release1{ session, "MyRelease1" };
|
|
||||||
ScopedRelease release2{ session, "MyRelease2" };
|
|
||||||
ScopedRelease release3{ session, "MyRelease3" };
|
|
||||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
|
||||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
|
||||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
EXPECT_EQ(release1->getSimilarReleases().size(), 0);
|
|
||||||
EXPECT_EQ(release2->getSimilarReleases().size(), 0);
|
|
||||||
EXPECT_EQ(release3->getSimilarReleases().size(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::list<ScopedTrack> tracks;
|
|
||||||
for (std::size_t i{}; i < 10; ++i)
|
|
||||||
{
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
|
|
||||||
if (i < 5)
|
|
||||||
tracks.back().get().modify()->setRelease(release1.get());
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tracks.back().get().modify()->setRelease(release2.get());
|
|
||||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks.emplace_back(session);
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
tracks.back().get().modify()->setRelease(release3.get());
|
|
||||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto releases{ release1->getSimilarReleases() };
|
|
||||||
ASSERT_EQ(releases.size(), 1);
|
|
||||||
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto releases{ release2->getSimilarReleases() };
|
|
||||||
ASSERT_EQ(releases.size(), 2);
|
|
||||||
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
|
||||||
EXPECT_EQ(releases[1]->getId(), release3.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackBookmark.hpp"
|
#include "database/objects/TrackBookmark.hpp"
|
||||||
#include "database/objects/TrackFeatures.hpp"
|
|
||||||
#include "database/objects/TrackList.hpp"
|
#include "database/objects/TrackList.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
|
||||||
@@ -142,9 +141,8 @@ namespace lms::db::tests
|
|||||||
class DatabaseFixture : public ::testing::Test
|
class DatabaseFixture : public ::testing::Test
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
~DatabaseFixture();
|
~DatabaseFixture() override;
|
||||||
|
|
||||||
public:
|
|
||||||
static void SetUpTestCase();
|
static void SetUpTestCase();
|
||||||
static void TearDownTestCase();
|
static void TearDownTestCase();
|
||||||
|
|
||||||
|
|||||||
@@ -102,9 +102,3 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
|
||||||
return RUN_ALL_TESTS();
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2021 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 "Common.hpp"
|
|
||||||
|
|
||||||
#include "database/objects/TrackFeatures.hpp"
|
|
||||||
|
|
||||||
namespace lms::db::tests
|
|
||||||
{
|
|
||||||
using ScopedTrackFeatures = ScopedEntity<db::TrackFeatures>;
|
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, TrackFeatures)
|
|
||||||
{
|
|
||||||
ScopedTrack track{ session };
|
|
||||||
ScopedUser user{ session, "MyUser" };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
EXPECT_EQ(TrackFeatures::getCount(session), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedTrackFeatures trackFeatures{ session, track.lockAndGet(), "" };
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
EXPECT_EQ(TrackFeatures::getCount(session), 1);
|
|
||||||
|
|
||||||
auto allTrackFeatures{ TrackFeatures::find(session) };
|
|
||||||
ASSERT_EQ(allTrackFeatures.results.size(), 1);
|
|
||||||
EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace lms::db::tests
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
add_library(lmsmath INTERFACE)
|
||||||
|
|
||||||
|
target_include_directories(lmsmath INTERFACE
|
||||||
|
include
|
||||||
|
)
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_subdirectory(test)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (BUILD_BENCHMARKS)
|
||||||
|
add_subdirectory(bench)
|
||||||
|
endif()
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
add_executable(bench-math
|
||||||
|
ChamferDistance.cpp
|
||||||
|
CosineDistance.cpp
|
||||||
|
DotProduct.cpp
|
||||||
|
EuclideanDistance.cpp
|
||||||
|
FFT.cpp
|
||||||
|
Math.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(bench-math PRIVATE
|
||||||
|
lmscore
|
||||||
|
lmsmath
|
||||||
|
benchmark
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_options(bench-math PRIVATE
|
||||||
|
$<$<NOT:$<CONFIG:Debug>>:-ffast-math>
|
||||||
|
)
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cmath>
|
||||||
|
#include <random>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "core/Random.hpp"
|
||||||
|
|
||||||
|
#include "math/ChamferDistance.hpp"
|
||||||
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
namespace lms::core::benchs
|
||||||
|
{
|
||||||
|
template<std::size_t Size>
|
||||||
|
struct BenchDistance
|
||||||
|
{
|
||||||
|
BenchDistance(const math::Vector<Size, float>& ref)
|
||||||
|
: _ref{ ref } {}
|
||||||
|
|
||||||
|
float operator()(const math::Vector<Size, float>& b) const
|
||||||
|
{
|
||||||
|
float sum{};
|
||||||
|
for (std::size_t i{}; i < Size; ++i)
|
||||||
|
{
|
||||||
|
const float diff{ _ref[i] - b[i] };
|
||||||
|
sum += diff * diff;
|
||||||
|
}
|
||||||
|
return std::sqrt(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
const math::Vector<Size, float>& _ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<std::size_t VectorSize, std::size_t SetASize, std::size_t SetBSize>
|
||||||
|
static void BM_ChamferDistanceAtoB(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
std::vector<math::Vector<VectorSize, float>> vecA;
|
||||||
|
std::vector<math::Vector<VectorSize, float>> vecB;
|
||||||
|
vecA.reserve(SetASize);
|
||||||
|
vecB.reserve(SetBSize);
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < SetASize; ++i)
|
||||||
|
{
|
||||||
|
auto& vec{ vecA.emplace_back() };
|
||||||
|
core::random::fillContainer(randomEngine, vec, 0.F, 1.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < SetBSize; ++i)
|
||||||
|
{
|
||||||
|
auto& vec{ vecB.emplace_back() };
|
||||||
|
core::random::fillContainer(randomEngine, vec, 0.F, 1.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(math::chamferDistanceAtoB<BenchDistance<VectorSize>>(vecA, vecB));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * SetASize * SetBSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t VectorSize, std::size_t SetASize, std::size_t SetBSize>
|
||||||
|
static void BM_SymmetricalChamferDistance(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
std::vector<math::Vector<VectorSize, float>> vecA;
|
||||||
|
std::vector<math::Vector<VectorSize, float>> vecB;
|
||||||
|
vecA.reserve(SetASize);
|
||||||
|
vecB.reserve(SetBSize);
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < SetASize; ++i)
|
||||||
|
{
|
||||||
|
auto& vec{ vecA.emplace_back() };
|
||||||
|
core::random::fillContainer(randomEngine, vec, 0.F, 1.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < SetBSize; ++i)
|
||||||
|
{
|
||||||
|
auto& vec{ vecB.emplace_back() };
|
||||||
|
core::random::fillContainer(randomEngine, vec, 0.F, 1.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(math::symmetricalChamferDistance<BenchDistance<VectorSize>>(vecA, vecB));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * 2 * SetASize * SetBSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmarks with different configurations
|
||||||
|
BENCHMARK_TEMPLATE(BM_ChamferDistanceAtoB, 128, 10, 10);
|
||||||
|
BENCHMARK_TEMPLATE(BM_ChamferDistanceAtoB, 128, 50, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_ChamferDistanceAtoB, 256, 10, 10);
|
||||||
|
BENCHMARK_TEMPLATE(BM_ChamferDistanceAtoB, 256, 50, 50);
|
||||||
|
|
||||||
|
BENCHMARK_TEMPLATE(BM_SymmetricalChamferDistance, 128, 10, 10);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SymmetricalChamferDistance, 128, 50, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SymmetricalChamferDistance, 256, 10, 10);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SymmetricalChamferDistance, 256, 50, 50);
|
||||||
|
} // namespace lms::core::benchs
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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 <benchmark/benchmark.h>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "core/Random.hpp"
|
||||||
|
|
||||||
|
#include "math/CosineDistance.hpp"
|
||||||
|
#include "math/NormalizedCosineDistance.hpp"
|
||||||
|
|
||||||
|
namespace lms::math::benchs
|
||||||
|
{
|
||||||
|
template<std::size_t Size>
|
||||||
|
static void BM_CosineDistance(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
Vector<Size, float> vec1;
|
||||||
|
Vector<Size, float> vec2;
|
||||||
|
|
||||||
|
core::random::fillContainer(randomEngine, vec1, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, vec2, 0.F, 1.F);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(computeCosineDistance(vec1, vec2));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t Size>
|
||||||
|
static void BM_NormalizedCosineDistance(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
Vector<Size, float> vec1;
|
||||||
|
Vector<Size, float> vec2;
|
||||||
|
|
||||||
|
core::random::fillContainer(randomEngine, vec1, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, vec2, 0.F, 1.F);
|
||||||
|
|
||||||
|
// normalize once, the normalized distance assumes L2-normalized vectors
|
||||||
|
vec1.normalizeL2();
|
||||||
|
vec2.normalizeL2();
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(computeNormalizedCosineDistance(vec1, vec2));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t Size>
|
||||||
|
static void BM_NormalizedCosineDistance_Functor(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
Vector<Size, float> vec1;
|
||||||
|
Vector<Size, float> vec2;
|
||||||
|
|
||||||
|
core::random::fillContainer(randomEngine, vec1, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, vec2, 0.F, 1.F);
|
||||||
|
|
||||||
|
vec1.normalizeL2();
|
||||||
|
vec2.normalizeL2();
|
||||||
|
|
||||||
|
const NormalizedCosineDistance<Size, float> dist{ vec1 };
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(dist(vec2));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK_TEMPLATE(BM_CosineDistance, 4);
|
||||||
|
BENCHMARK_TEMPLATE(BM_CosineDistance, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_CosineDistance, 160);
|
||||||
|
BENCHMARK_TEMPLATE(BM_NormalizedCosineDistance, 4);
|
||||||
|
BENCHMARK_TEMPLATE(BM_NormalizedCosineDistance, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_NormalizedCosineDistance, 160);
|
||||||
|
BENCHMARK_TEMPLATE(BM_NormalizedCosineDistance_Functor, 4);
|
||||||
|
BENCHMARK_TEMPLATE(BM_NormalizedCosineDistance_Functor, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_NormalizedCosineDistance_Functor, 160);
|
||||||
|
} // namespace lms::math::benchs
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 <benchmark/benchmark.h>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "core/Random.hpp"
|
||||||
|
|
||||||
|
#include "math/DotProduct.hpp"
|
||||||
|
|
||||||
|
namespace lms::math::benchs
|
||||||
|
{
|
||||||
|
template<std::size_t Size>
|
||||||
|
static void BM_DotProduct(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
Vector<Size, float> vec1;
|
||||||
|
Vector<Size, float> vec2;
|
||||||
|
|
||||||
|
core::random::fillContainer(randomEngine, vec1, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, vec2, 0.F, 1.F);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(computeDotProduct(vec1, vec2));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK_TEMPLATE(BM_DotProduct, 4);
|
||||||
|
BENCHMARK_TEMPLATE(BM_DotProduct, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_DotProduct, 160);
|
||||||
|
} // namespace lms::math::benchs
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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 <benchmark/benchmark.h>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "core/Random.hpp"
|
||||||
|
|
||||||
|
#include "math/EuclideanDistance.hpp"
|
||||||
|
|
||||||
|
namespace lms::core::benchs
|
||||||
|
{
|
||||||
|
template<std::size_t Size>
|
||||||
|
static void BM_SquaredEuclideanDistance(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
math::Vector<Size, float> vec1;
|
||||||
|
math::Vector<Size, float> vec2;
|
||||||
|
|
||||||
|
core::random::fillContainer(randomEngine, vec1, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, vec2, 0.F, 1.F);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(math::computeEuclideanSquaredDistance(vec1, vec2));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t Size>
|
||||||
|
static void BM_SquaredEuclideanDistanceWithWeights(benchmark::State& state)
|
||||||
|
{
|
||||||
|
std::minstd_rand randomEngine{ 0 };
|
||||||
|
|
||||||
|
math::Vector<Size, float> vec1;
|
||||||
|
math::Vector<Size, float> vec2;
|
||||||
|
math::Vector<Size, float> weights;
|
||||||
|
|
||||||
|
core::random::fillContainer(randomEngine, vec1, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, vec2, 0.F, 1.F);
|
||||||
|
core::random::fillContainer(randomEngine, weights, 0.F, 1.F);
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
benchmark::DoNotOptimize(math::computeEuclideanSquaredDistanceWithWeights(vec1, vec2, weights));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations() * Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK_TEMPLATE(BM_SquaredEuclideanDistance, 4);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SquaredEuclideanDistance, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SquaredEuclideanDistance, 160);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SquaredEuclideanDistanceWithWeights, 4);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SquaredEuclideanDistanceWithWeights, 50);
|
||||||
|
BENCHMARK_TEMPLATE(BM_SquaredEuclideanDistanceWithWeights, 160);
|
||||||
|
} // namespace lms::core::benchs
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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 <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <complex>
|
||||||
|
#include <numbers>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "core/AlignedHeapArray.hpp"
|
||||||
|
|
||||||
|
#include "math/FFT.hpp"
|
||||||
|
|
||||||
|
namespace lms::math::benchs
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template<typename FloatType>
|
||||||
|
std::vector<FloatType> generateTestSignal(std::size_t n)
|
||||||
|
{
|
||||||
|
std::vector<FloatType> data(n);
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < n; ++i)
|
||||||
|
data[i] = std::sin(static_cast<FloatType>(2) * std::numbers::pi_v<FloatType> * static_cast<FloatType>(i) / static_cast<FloatType>(n));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
template<std::size_t N, typename FloatType>
|
||||||
|
void BM_FFT(benchmark::State& state)
|
||||||
|
{
|
||||||
|
const std::vector<FloatType> inputSignal{ generateTestSignal<FloatType>(N) };
|
||||||
|
|
||||||
|
FixedRealFFTPlan<N, FloatType> fft;
|
||||||
|
core::AlignedHeapArray<FloatType, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
|
||||||
|
core::AlignedHeapArray<std::complex<FloatType>, FixedRealFFTPlan<N>::minBufferAlignment> output{ fft.getOutputSize() };
|
||||||
|
|
||||||
|
std::copy(inputSignal.begin(), inputSignal.end(), input.begin());
|
||||||
|
|
||||||
|
for (auto _ : state)
|
||||||
|
fft.apply({ input.data(), input.size() }, { output.data(), output.size() });
|
||||||
|
|
||||||
|
state.counters["Samples/s"] = benchmark::Counter{ static_cast<double>(N), benchmark::Counter::kIsIterationInvariantRate };
|
||||||
|
state.counters["FFT/s"] = benchmark::Counter{ 1.0, benchmark::Counter::kIsIterationInvariantRate };
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK(BM_FFT<512, float>);
|
||||||
|
BENCHMARK(BM_FFT<1024, float>);
|
||||||
|
BENCHMARK(BM_FFT<2048, float>);
|
||||||
|
BENCHMARK(BM_FFT<512, double>);
|
||||||
|
BENCHMARK(BM_FFT<1024, double>);
|
||||||
|
BENCHMARK(BM_FFT<2048, double>);
|
||||||
|
|
||||||
|
} // namespace lms::math::benchs
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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 <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cassert>
|
||||||
|
#include <span>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace lms::math
|
||||||
|
{
|
||||||
|
template<typename VectorType>
|
||||||
|
class CentroidCalculator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static_assert(!std::is_const_v<VectorType>);
|
||||||
|
|
||||||
|
using value_type = typename VectorType::value_type;
|
||||||
|
using size_type = std::size_t;
|
||||||
|
|
||||||
|
constexpr void add(const VectorType& value)
|
||||||
|
{
|
||||||
|
_sum += value;
|
||||||
|
++_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename InputIt>
|
||||||
|
constexpr void add(InputIt first, InputIt last)
|
||||||
|
{
|
||||||
|
for (; first != last; ++first)
|
||||||
|
add(*first);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr VectorType finalize() const
|
||||||
|
{
|
||||||
|
assert(_count > 0);
|
||||||
|
VectorType result{ _sum };
|
||||||
|
result *= static_cast<value_type>(1) / static_cast<value_type>(_count);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr VectorType finalizeNormalized() const
|
||||||
|
{
|
||||||
|
VectorType result{ finalize() };
|
||||||
|
result.normalizeL2();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool empty() const
|
||||||
|
{
|
||||||
|
return _count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_type count() const
|
||||||
|
{
|
||||||
|
return _count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
VectorType _sum;
|
||||||
|
size_type _count{};
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename VectorType>
|
||||||
|
constexpr VectorType computeCentroid(std::span<const VectorType> values)
|
||||||
|
{
|
||||||
|
CentroidCalculator<VectorType> calculator;
|
||||||
|
calculator.add(std::cbegin(values), std::cend(values));
|
||||||
|
return calculator.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename VectorType>
|
||||||
|
constexpr VectorType computeNormalizedCentroid(std::span<const VectorType> values)
|
||||||
|
{
|
||||||
|
CentroidCalculator<VectorType> calculator;
|
||||||
|
calculator.add(std::cbegin(values), std::cend(values));
|
||||||
|
return calculator.finalizeNormalized();
|
||||||
|
}
|
||||||
|
} // namespace lms::math
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cassert>
|
||||||
|
#include <concepts>
|
||||||
|
#include <limits>
|
||||||
|
#include <ranges>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace lms::math
|
||||||
|
{
|
||||||
|
/// Computes the Chamfer distance from set A to set B.
|
||||||
|
/// For each element in A, finds the nearest element in B and sums these minimum distances.
|
||||||
|
/// The result is normalized by the size of A.
|
||||||
|
///
|
||||||
|
/// @tparam DistanceFunc A functor type: constructed with an element of A as ref,
|
||||||
|
/// then called with each element of B as target.
|
||||||
|
/// @param A A forward range of vectors
|
||||||
|
/// @param B A forward range of vectors (same element type as A)
|
||||||
|
/// @return The normalized sum of minimum distances from A to B
|
||||||
|
template<typename DistanceFunc,
|
||||||
|
std::ranges::forward_range RangeA,
|
||||||
|
std::ranges::forward_range RangeB>
|
||||||
|
requires std::same_as<std::ranges::range_value_t<RangeA>,
|
||||||
|
std::ranges::range_value_t<RangeB>>
|
||||||
|
auto chamferDistanceAtoB(const RangeA& A, const RangeB& B)
|
||||||
|
{
|
||||||
|
using Vector = std::ranges::range_value_t<RangeA>;
|
||||||
|
using ValueType = std::invoke_result_t<DistanceFunc, const Vector&>;
|
||||||
|
|
||||||
|
assert(!std::ranges::empty(A));
|
||||||
|
assert(!std::ranges::empty(B));
|
||||||
|
|
||||||
|
ValueType total{};
|
||||||
|
std::size_t countA{};
|
||||||
|
|
||||||
|
for (const auto& a : A)
|
||||||
|
{
|
||||||
|
DistanceFunc distFunc{ a };
|
||||||
|
ValueType bestDist{ std::numeric_limits<ValueType>::max() };
|
||||||
|
|
||||||
|
for (const auto& b : B)
|
||||||
|
{
|
||||||
|
const ValueType dist{ distFunc(b) };
|
||||||
|
if (dist < bestDist)
|
||||||
|
bestDist = dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
total += bestDist;
|
||||||
|
++countA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total / static_cast<ValueType>(countA);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the symmetrical Chamfer distance between two sets.
|
||||||
|
/// Returns the average of chamferDistanceAtoB(A, B) and chamferDistanceAtoB(B, A).
|
||||||
|
///
|
||||||
|
/// @tparam DistanceFunc A functor type: constructed with the ref element,
|
||||||
|
/// then called with each candidate element.
|
||||||
|
/// @param A A forward range of vectors
|
||||||
|
/// @param B A forward range of vectors (same element type as A)
|
||||||
|
/// @return The symmetrical Chamfer distance
|
||||||
|
template<typename DistanceFunc,
|
||||||
|
std::ranges::forward_range RangeA,
|
||||||
|
std::ranges::forward_range RangeB>
|
||||||
|
requires std::same_as<std::ranges::range_value_t<RangeA>,
|
||||||
|
std::ranges::range_value_t<RangeB>>
|
||||||
|
auto symmetricalChamferDistance(const RangeA& A, const RangeB& B)
|
||||||
|
{
|
||||||
|
const auto aToB{ chamferDistanceAtoB<DistanceFunc>(A, B) };
|
||||||
|
const auto bToA{ chamferDistanceAtoB<DistanceFunc>(B, A) };
|
||||||
|
return (aToB + bToA) / static_cast<decltype(aToB)>(2);
|
||||||
|
}
|
||||||
|
} // namespace lms::math
|
||||||
@@ -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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
namespace lms::math
|
||||||
|
{
|
||||||
|
template<std::size_t Size, typename FloatType>
|
||||||
|
FloatType computeCosineDistance(const Vector<Size, FloatType>& a, const Vector<Size, FloatType>& b)
|
||||||
|
{
|
||||||
|
constexpr FloatType smallEpsilon{ 1e-12F };
|
||||||
|
|
||||||
|
FloatType dot{};
|
||||||
|
FloatType lhsNormSquared{};
|
||||||
|
FloatType rhsNormSquared{};
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < Size; ++i)
|
||||||
|
{
|
||||||
|
dot += a[i] * b[i];
|
||||||
|
lhsNormSquared += a[i] * a[i];
|
||||||
|
rhsNormSquared += b[i] * b[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FloatType denom{ std::sqrt(lhsNormSquared * rhsNormSquared) };
|
||||||
|
if (denom <= smallEpsilon)
|
||||||
|
return FloatType{ 1.F };
|
||||||
|
|
||||||
|
FloatType cosineSimilarity{ dot / denom };
|
||||||
|
cosineSimilarity = std::clamp(cosineSimilarity, FloatType{ -1.F }, FloatType{ 1.F });
|
||||||
|
|
||||||
|
return FloatType{ 1.F } - cosineSimilarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t Size, typename FloatType = float>
|
||||||
|
struct CosineDistance
|
||||||
|
{
|
||||||
|
constexpr CosineDistance(const Vector<Size, FloatType>& ref)
|
||||||
|
: _ref{ ref }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatType operator()(const Vector<Size, FloatType>& a) const
|
||||||
|
{
|
||||||
|
return computeCosineDistance(_ref, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vector<Size, FloatType>& _ref;
|
||||||
|
};
|
||||||
|
} // namespace lms::math
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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 <type_traits>
|
||||||
|
|
||||||
|
#include "math/SquareMatrix.hpp"
|
||||||
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
namespace lms::math
|
||||||
|
{
|
||||||
|
template<std::size_t Size, typename FloatType = float>
|
||||||
|
class CovarianceMatrixCalculator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static_assert(!std::is_const_v<FloatType>);
|
||||||
|
|
||||||
|
using CovarianceMatrix = SquareMatrix<FloatType, Size>;
|
||||||
|
|
||||||
|
constexpr void add(const Vector<Size, FloatType>& centeredVector)
|
||||||
|
{
|
||||||
|
for (std::size_t i{}; i < Size; ++i)
|
||||||
|
{
|
||||||
|
for (std::size_t j{}; j <= i; ++j)
|
||||||
|
_cov[i][j] += centeredVector[i] * centeredVector[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
++_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename InputIt>
|
||||||
|
constexpr void add(InputIt first, InputIt last)
|
||||||
|
{
|
||||||
|
for (; first != last; ++first)
|
||||||
|
add(*first);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void finalizeSample(CovarianceMatrix& out) const
|
||||||
|
{
|
||||||
|
out.fill(FloatType{});
|
||||||
|
if (_count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const FloatType divisor{ static_cast<FloatType>(_count - 1) };
|
||||||
|
for (std::size_t i{}; i < Size; ++i)
|
||||||
|
{
|
||||||
|
for (std::size_t j{}; j <= i; ++j)
|
||||||
|
{
|
||||||
|
const FloatType value{ _cov[i][j] / divisor };
|
||||||
|
out[i][j] = value;
|
||||||
|
out[j][i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void finalizePopulation(CovarianceMatrix& out) const
|
||||||
|
{
|
||||||
|
out.fill(FloatType{});
|
||||||
|
if (_count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const FloatType divisor{ static_cast<FloatType>(_count) };
|
||||||
|
for (std::size_t i{}; i < Size; ++i)
|
||||||
|
{
|
||||||
|
for (std::size_t j{}; j <= i; ++j)
|
||||||
|
{
|
||||||
|
const FloatType value{ _cov[i][j] / divisor };
|
||||||
|
out[i][j] = value;
|
||||||
|
out[j][i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool empty() const
|
||||||
|
{
|
||||||
|
return _count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::size_t count() const
|
||||||
|
{
|
||||||
|
return _count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CovarianceMatrix _cov{};
|
||||||
|
std::size_t _count{};
|
||||||
|
};
|
||||||
|
} // namespace lms::math
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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 "math/Vector.hpp"
|
||||||
|
|
||||||
|
namespace lms::math
|
||||||
|
{
|
||||||
|
template<std::size_t Size, typename FloatType>
|
||||||
|
constexpr FloatType computeDotProduct(const Vector<Size, FloatType>& a, const Vector<Size, FloatType>& b)
|
||||||
|
{
|
||||||
|
FloatType res{};
|
||||||
|
|
||||||
|
for (std::size_t i{}; i < Size; ++i)
|
||||||
|
res += a[i] * b[i];
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::size_t Size, typename FloatType = float>
|
||||||
|
struct DotProduct
|
||||||
|
{
|
||||||
|
constexpr DotProduct(const Vector<Size, FloatType>& ref)
|
||||||
|
: _ref{ ref }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr FloatType operator()(const Vector<Size, FloatType>& a) const
|
||||||
|
{
|
||||||
|
return computeDotProduct(_ref, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vector<Size, FloatType>& _ref;
|
||||||
|
};
|
||||||
|
} // namespace lms::math
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 <cmath>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace lms::math
|
||||||
|
{
|
||||||
|
template<typename FloatType = float>
|
||||||
|
FloatType entropy(std::span<const FloatType> c)
|
||||||
|
{
|
||||||
|
FloatType sum{};
|
||||||
|
|
||||||
|
for (auto v : c)
|
||||||
|
sum += v;
|
||||||
|
|
||||||
|
constexpr FloatType epsilon{ 1e-12 };
|
||||||
|
|
||||||
|
if (sum <= epsilon)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const FloatType invSum{ FloatType{ 1 } / sum };
|
||||||
|
|
||||||
|
FloatType res{};
|
||||||
|
for (auto v : c)
|
||||||
|
{
|
||||||
|
if (v <= epsilon)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const FloatType p{ v * invSum };
|
||||||
|
res -= p * std::log(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} // namespace lms::math
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user