Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ compile_commands.json
# CMake typical build dirs
/cmake_build*
/cmake-build*

/build

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's here by mistake, was building in this folder by default, removed

180 changes: 180 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,82 @@ option(SYSTEM_DATE_H "Use system installation of date.h" OFF)
option(SYSTEM_SQLITE "Use system installation of SQLite" ON)
option(SYSTEM_SQLITE_MODERN_CPP "Use system installation of sqlite_modern_cpp" OFF)


# The processor's own AES instructions are worth two orders of magnitude over
# software AES when reading an encrypted OneLibrary database, so use them where
# the compiler can be persuaded to emit them. Whether the processor running the
# built library has them is a separate question, decided at run time; settled
# here is only whether the intrinsics compile, and with which flags. They are
# applied to `aes_hardware.cpp` alone, so no other translation unit can pick up
# an instruction the target may not have.
include(CheckCXXSourceCompiles)

set(DJINTEROP_AES_X86_SOURCE "
#if defined(_MSC_VER)
#include <intrin.h>
#else
#include <immintrin.h>
#endif
int main()
{
__m128i x = _mm_setzero_si128();
x = _mm_aesenc_si128(x, x);
x = _mm_aesenclast_si128(x, x);
x = _mm_aesdec_si128(x, x);
x = _mm_aesdeclast_si128(x, x);
return _mm_cvtsi128_si32(x);
}
")

set(DJINTEROP_AES_ARM64_SOURCE "
#include <arm_neon.h>
int main()
{
uint8x16_t x = vdupq_n_u8(0);
x = vaesmcq_u8(vaeseq_u8(x, x));
x = vaesimcq_u8(vaesdq_u8(x, x));
return vgetq_lane_u8(x, 0);
}
")

set(DJINTEROP_AES_INTRINSICS_DEFINE "")
set(DJINTEROP_AES_INTRINSICS_FLAGS "")

# Try one architecture, unless another has already answered. Bare comes first:
# MSVC needs no flags at all, and neither does a compiler whose default target
# already includes the extension, such as Apple's on arm64.
macro(djinterop_try_aes arch source flags)
if(NOT DJINTEROP_AES_INTRINSICS_DEFINE)
check_cxx_source_compiles("${source}" DJINTEROP_AES_${arch}_BARE)
if(NOT DJINTEROP_AES_${arch}_BARE)
set(CMAKE_REQUIRED_FLAGS "${flags}")
check_cxx_source_compiles(
"${source}" DJINTEROP_AES_${arch}_FLAGGED)
unset(CMAKE_REQUIRED_FLAGS)
if(DJINTEROP_AES_${arch}_FLAGGED)
set(DJINTEROP_AES_INTRINSICS_FLAGS "${flags}")
endif()
endif()
if(DJINTEROP_AES_${arch}_BARE OR DJINTEROP_AES_${arch}_FLAGGED)
set(DJINTEROP_AES_INTRINSICS_DEFINE
"DJINTEROP_AES_INTRINSICS_${arch}")
endif()
endif()
endmacro()

djinterop_try_aes(X86 "${DJINTEROP_AES_X86_SOURCE}" "-maes -msse2")
djinterop_try_aes(ARM64 "${DJINTEROP_AES_ARM64_SOURCE}" "-march=armv8-a+crypto")

if(DJINTEROP_AES_INTRINSICS_DEFINE)
message(STATUS "Using AES instructions where available at run time")
set_source_files_properties(
src/djinterop/util/crypto/aes_hardware.cpp PROPERTIES
COMPILE_DEFINITIONS "${DJINTEROP_AES_INTRINSICS_DEFINE}"
COMPILE_FLAGS "${DJINTEROP_AES_INTRINSICS_FLAGS}")
else()
message(STATUS "No AES instructions for this target; using AES tables")
endif()

add_library(
DjInterop
include/djinterop/album_art.hpp
Expand Down Expand Up @@ -81,6 +157,7 @@ add_library(
include/djinterop/engine/v3/track_table.hpp
include/djinterop/exceptions.hpp
include/djinterop/musical_key.hpp
include/djinterop/onelibrary/onelibrary.hpp
include/djinterop/pad_color.hpp
include/djinterop/performance_data.hpp
include/djinterop/playlist.hpp
Expand Down Expand Up @@ -202,10 +279,34 @@ add_library(
src/djinterop/impl/playlist_impl.hpp
src/djinterop/impl/track_impl.cpp
src/djinterop/impl/track_impl.hpp
src/djinterop/onelibrary/content_table.cpp
src/djinterop/onelibrary/content_table.hpp
src/djinterop/onelibrary/crate_impl.cpp
src/djinterop/onelibrary/crate_impl.hpp
src/djinterop/onelibrary/database_impl.cpp
src/djinterop/onelibrary/database_impl.hpp
src/djinterop/onelibrary/onelibrary.cpp
src/djinterop/onelibrary/onelibrary_context.hpp
src/djinterop/onelibrary/playlist_impl.cpp
src/djinterop/onelibrary/playlist_impl.hpp
src/djinterop/onelibrary/playlist_table.cpp
src/djinterop/onelibrary/playlist_table.hpp
src/djinterop/onelibrary/track_impl.cpp
src/djinterop/onelibrary/track_impl.hpp
src/djinterop/playlist.cpp
src/djinterop/track.cpp
src/djinterop/util/chrono.cpp
src/djinterop/util/chrono.hpp
src/djinterop/util/crypto/aes.cpp
src/djinterop/util/crypto/aes.hpp
src/djinterop/util/crypto/aes_hardware.cpp
src/djinterop/util/crypto/aes_hardware.hpp
src/djinterop/util/crypto/sha512.cpp
src/djinterop/util/crypto/sha512.hpp
src/djinterop/util/crypto/sqlcipher_codec.cpp
src/djinterop/util/crypto/sqlcipher_codec.hpp
src/djinterop/util/crypto/sqlcipher_wal.cpp
src/djinterop/util/crypto/sqlcipher_wal.hpp
src/djinterop/util/filesystem.cpp
src/djinterop/util/filesystem.hpp
src/djinterop/util/random.cpp
Expand Down Expand Up @@ -239,6 +340,14 @@ target_include_directories(
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${DJINTEROP_INSTALL_INCLUDEDIR}>)

# Decrypting the pages of a database is spread over the processors available,
# which is what the standard threading library is needed for.
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(
DjInterop PRIVATE
Threads::Threads)

# Always rely on system installation of zlib.
set(ZLIB_MIN_VERSION 1.2.8)
find_package(ZLIB ${ZLIB_MIN_VERSION} REQUIRED)
Expand Down Expand Up @@ -273,6 +382,7 @@ if(SYSTEM_SQLITE)
target_link_libraries(
DjInterop PUBLIC
${SQLite3_LIBRARIES})
set(DJINTEROP_SQLITE_VERSION "${SQLite3_VERSION}")
else()
# Use bundled SQLite amalgamation sources.
message(STATUS "Using bundled SQLite...")
Expand All @@ -285,6 +395,16 @@ else()
target_include_directories(
DjInterop PRIVATE SYSTEM
ext/sqlite-amalgamation)

# Read the version out of the amalgamation rather than restating it here,
# so that bumping the bundled copy is a matter of replacing two files.
file(
STRINGS ext/sqlite-amalgamation/sqlite3.h DJINTEROP_SQLITE_VERSION
REGEX "^#define SQLITE_VERSION[ \t]+\"")
string(
REGEX REPLACE "^#define SQLITE_VERSION[ \t]+\"([^\"]+)\".*" "\\1"
DJINTEROP_SQLITE_VERSION "${DJINTEROP_SQLITE_VERSION}")
message(STATUS "Bundled SQLite is version ${DJINTEROP_SQLITE_VERSION}")
endif()

if(SYSTEM_SQLITE_MODERN_CPP)
Expand Down Expand Up @@ -357,6 +477,9 @@ install(FILES
include/djinterop/engine/v3/track_data_blob.hpp
include/djinterop/engine/v3/track_table.hpp
DESTINATION "${DJINTEROP_INSTALL_INCLUDEDIR}/engine/v3")
install(FILES
include/djinterop/onelibrary/onelibrary.hpp
DESTINATION "${DJINTEROP_INSTALL_INCLUDEDIR}/onelibrary")


if (UNIX)
Expand Down Expand Up @@ -407,6 +530,7 @@ if (BUILD_EXAMPLES)

add_djinterop_example(engine_prime)
add_djinterop_example(engine_library_v2_low_level)
add_djinterop_example(onelibrary)
endif()

# Unit tests.
Expand All @@ -431,6 +555,10 @@ if (Boost_FOUND)
${Boost_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR}/include
include)
# A test that compiles library sources into itself, rather than only
# linking, needs the headers that those sources include.
target_include_directories(${test_executable_name} PRIVATE SYSTEM
$<TARGET_PROPERTY:DjInterop,INCLUDE_DIRECTORIES>)
target_link_libraries(${test_executable_name} PUBLIC
DjInterop
${Boost_LIBRARIES})
Expand All @@ -450,6 +578,58 @@ if (Boost_FOUND)
add_djinterop_test(engine/v3/ performance_data_table_test)
add_djinterop_test(engine/v3/ track_table_test)

# Some parts of the library are internal, and its symbols are hidden, so
# tests of them compile those sources into themselves rather than linking.
#
# The OneLibrary tests are commented out below. Each of them talks to
# SQLite directly, and a test cannot borrow SQLite from the library: its
Comment on lines +600 to +601

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in database_test.cpp - I think it might be best if the tests only use the public API of libdjinterop and don't talk to SQLite directly.

# symbols are hidden there. With `-DSYSTEM_SQLITE=OFF` that means every
# one of them compiles the amalgamation into itself, some fifteen seconds
# apiece, on top of the copy the library already builds.
#
# The sources are still in `test/djinterop/onelibrary`; uncommenting the
# block below is all it takes to run them again. `sqlite3_deserialize`,
# which reading a decrypted database goes through, arrived in SQLite 3.36,
# hence the version the database test asks for.
#
# if(SYSTEM_SQLITE)
# set(DJINTEROP_TEST_SQLITE_SOURCES "")
# else()
# set(DJINTEROP_TEST_SQLITE_SOURCES ext/sqlite-amalgamation/sqlite3.c)
# endif()
#
# add_djinterop_test(onelibrary/ content_table_test)
# target_sources(onelibrary_content_table_test PRIVATE
# src/djinterop/onelibrary/content_table.cpp
# ${DJINTEROP_TEST_SQLITE_SOURCES})
#
# if(NOT DJINTEROP_SQLITE_VERSION VERSION_LESS 3.36)
# add_djinterop_test(onelibrary/ database_test)
# target_sources(onelibrary_database_test PRIVATE
# src/djinterop/util/crypto/aes.cpp
# src/djinterop/util/crypto/aes_hardware.cpp
# src/djinterop/util/crypto/sha512.cpp
# src/djinterop/util/crypto/sqlcipher_codec.cpp
# src/djinterop/util/filesystem.cpp
# ${DJINTEROP_TEST_SQLITE_SOURCES})
# else()
# message(
# STATUS
# "OneLibrary database test not available, as reading the format "
# "needs SQLite 3.36 or newer")
# endif()
#
# add_djinterop_test(onelibrary/ playlist_table_test)
# target_sources(onelibrary_playlist_table_test PRIVATE
# src/djinterop/onelibrary/playlist_table.cpp
# ${DJINTEROP_TEST_SQLITE_SOURCES})

add_djinterop_test(util/ crypto_test)
target_sources(util_crypto_test PRIVATE
src/djinterop/util/crypto/aes.cpp
src/djinterop/util/crypto/aes_hardware.cpp
src/djinterop/util/crypto/sha512.cpp
src/djinterop/util/crypto/sqlcipher_codec.cpp)
else()
message(STATUS "Unit tests not available, as Boost cannot be found")
endif()
Expand Down
2 changes: 2 additions & 0 deletions DjInteropConfig.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")

include(CMakeFindDependencyMacro)
find_dependency(ZLIB)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_dependency(Threads)
if(DJINTEROP_SYSTEM_DATE_H)
find_dependency(date)
endif()
Expand Down
39 changes: 36 additions & 3 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,42 @@ As such, in order to create a new library or load an existing library with the
intention of operating on it using the high-level API, it is always necessary
to start with format-specific functions to do so:

| Library Type | Include Path |
|--------------|------------------------------------------|
| Engine | `#include <djinterop/engine/engine.hpp>` |
| Library Type | Include Path |
|--------------|--------------------------------------------------|
| Engine | `#include <djinterop/engine/engine.hpp>` |
| OneLibrary | `#include <djinterop/onelibrary/onelibrary.hpp>` |

OneLibrary
----------

The AlphaTheta OneLibrary format, also documented as Device Library Plus, is
the successor to the DeviceSQL `export.pdb` library that rekordbox wrote to USB
media. A database is loaded by way of `onelibrary::load_database()`, given
either the root directory of a device or the database file itself. A number of
aspects of the format are worth noting:

* Support is currently read-only, and everything that would change a database
throws `djinterop::unsupported_operation`.
* Beat grids, waveforms, hot cues and loops are not held in the database.
rekordbox leaves them in the ANLZ files that `content.analysisDataFilePath`
points at, and does not populate the `cue` table on export. Those accessors
therefore return nothing rather than throwing. A caller that reads ANLZ
files itself can load the device as an `onelibrary::library`, whose
`analysis_path()` gives the path recorded for a track, relative to the root
of the device; `library::db()` then gives the same database that
`load_database()` would have.
* `onelibrary::library` also reaches the two other things a device carries
that the format-agnostic interface has nowhere to put: `key_name()` gives
the musical key in the notation rekordbox wrote, which may be Camelot and
which `track::key()` cannot represent, and `color_id()` gives the colour the
DJ marked a track with, numbered as `export.pdb` numbers them.
* The format has a single tree that serves as both playlists and crates, so
`playlists_and_crates_are_distinct` is false and the two views show the same
rows.
* A device is read by decrypting it into memory. rekordbox writes the library
in write-ahead-logged mode, and the log has to be folded in before SQLite
sees the file, so reading one needs SQLite 3.36 or newer, built without
`SQLITE_OMIT_DESERIALIZE`.


Stable API/ABI
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ State of Support
================

The library is currently in development, and not all features are implemented
yet. It currently supports only the Engine Library format.
yet. It supports the Engine Library format, and can read the AlphaTheta
OneLibrary format.

What is supported:

Expand All @@ -27,11 +28,20 @@ The library supports the following firmware and application versions:
SC6000/M) may work, but this is currently untested.
* Engine DJ Desktop (aka Engine Prime) from 1.0.1 to 4.3.0.

The library also reads the AlphaTheta OneLibrary format, which rekordbox 7
writes to USB media as `PIONEER/rekordbox/exportLibrary.db`, and which players
from the CDJ-3000X, XDJ-AZ, OPUS-QUAD and OMNIS-DUO onwards read, as does the
CDJ-3000 from firmware 3.15. Track metadata, playlists and crates can be read.
Writing is not supported yet, and beat grids, waveforms, hot cues and loops are
not held in the database at all, as rekordbox leaves them in the ANLZ files
beside it. The format is also documented under the name Device Library Plus.

What is not supported (yet):

* Album art
* Play history
* DJ record libraries in formats other than Engine Prime
* Writing OneLibrary databases
* DJ record libraries in formats other than Engine Prime and OneLibrary

How Do I Use It?
================
Expand Down
33 changes: 29 additions & 4 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,34 @@ Overview
This directory contains small example applications that illustrate the use
of `libdjinterop`.

This application can be minimally compiled in isolation with an invocation
similar to the below (adjust for your favourite compiler as appropriate):
| Example | What it does |
|-------------------------------|---------------------------------------------------------------------|
| `engine_prime` | Writes a track, a crate and a playlist to an Engine Prime library. |
| `engine_library_v2_low_level` | Uses the low-level Engine v2 API to work with tables directly. |
| `onelibrary` | Prints the tracks and playlists of an AlphaTheta OneLibrary device. |

Each application can be minimally compiled in isolation with an invocation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a realistic use case, compiling the examples in isolation?

If the user has gone to the trouble of cloning this repo, is there some advantage offered by compiling in isolation rather than the CMake targets that are already there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just used the same pattern you introduced, but don't think it's really nessessary, cleaned up

similar to the below (adjust for your favourite compiler as appropriate). The
library needs a C++20 compiler, and its headers refuse to compile under any
older standard:

```shell
g++ -std=c++20 `pkg-config --cflags djinterop` engine_prime.cpp `pkg-config --libs djinterop` -o engine_prime
```

Any of the others is compiled the same way, by name:

```shell
g++ -std=c++20 `pkg-config --cflags djinterop` onelibrary.cpp `pkg-config --libs djinterop` -o onelibrary
```

They are also built by the project itself, as `example_engine_prime` and so
on, when it is configured with `-DBUILD_EXAMPLES=ON`.

`onelibrary` takes the device to read as its argument, either the root
directory of a device or the `exportLibrary.db` file itself, and optionally a
passphrase:

```shell
g++ -std=c++17 `pkg-config --cflags djinterop` engine_prime.cpp `pkg-config --libs djinterop`
```
./onelibrary /Volumes/MYUSB
```
Loading
Loading