diff options
author | IOhannes m zmölnig <zmoelnig@umlautS.umlaeute.mur.at> | 2021-01-02 20:46:28 +0100 |
---|---|---|
committer | IOhannes m zmölnig <zmoelnig@umlautS.umlaeute.mur.at> | 2021-01-02 20:46:28 +0100 |
commit | 8aedadd869752a59c32d6c69a346aaeab52fe8fa (patch) | |
tree | a6bb64abdb36de09d24395e96a28f54d41a533d8 | |
parent | 25458a95d45b2ce741b76cb4d9d8de1a91f2dce6 (diff) |
New upstream version 3.0.3+dfsg
85 files changed, 1578 insertions, 1592 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index 16f14a3..8ae74ff 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -8,6 +8,7 @@ branches: image: - Visual Studio 2015 - Visual Studio 2017 + - Visual Studio 2019 environment: matrix: @@ -15,55 +16,45 @@ environment: - AUDIO_DRIVER: Asio CONFIGURATION: Release GENERATOR: Visual Studio 12 2013 - QT_VERSION: 5.6 - QT: msvc2013 # Visual Studio 2013, 64-bit Release, Asio driver - AUDIO_DRIVER: Asio CONFIGURATION: Release GENERATOR: Visual Studio 12 2013 Win64 - QT_VERSION: 5.10 - QT: msvc2013_64 # Visual Studio 2013, 64-bit Debug, Wasapi driver - AUDIO_DRIVER: Wasapi CONFIGURATION: Debug GENERATOR: Visual Studio 12 2013 Win64 - QT_VERSION: 5.10 - QT: msvc2013_64 # Visual Studio 2015, 32-bit Release, Asio driver - AUDIO_DRIVER: Asio CONFIGURATION: Release GENERATOR: Visual Studio 14 2015 - QT_VERSION: 5.10 - QT: msvc2015 # Visual Studio 2015, 64-bit Debug, Asio driver - AUDIO_DRIVER: Asio CONFIGURATION: Debug GENERATOR: Visual Studio 14 2015 Win64 - QT_VERSION: 5.10 - QT: msvc2015_64 # Visual Studio 2015, 64-bit Release, Asio driver - AUDIO_DRIVER: Asio CONFIGURATION: Release GENERATOR: Visual Studio 14 2015 Win64 - QT_VERSION: 5.10 - QT: msvc2015_64 # Visual Studio 2015, 64-bit Release, Wasapi driver - AUDIO_DRIVER: Wasapi CONFIGURATION: Release GENERATOR: Visual Studio 14 2015 Win64 - QT_VERSION: 5.10 - QT: msvc2015_64 # Visual Studio 2017, 64-bit Release, Asio driver - AUDIO_DRIVER: Asio CONFIGURATION: Release GENERATOR: Visual Studio 15 2017 Win64 - QT_VERSION: 5.10 - QT: msvc2017_64 + # Visual Studio 2019, 64-bit Release, Asio driver + - AUDIO_DRIVER: Asio + CONFIGURATION: Release + GENERATOR: Visual Studio 16 2019 matrix: exclude: - image: Visual Studio 2015 GENERATOR: Visual Studio 15 2017 Win64 + - image: Visual Studio 2015 + GENERATOR: Visual Studio 16 2019 - image: Visual Studio 2017 GENERATOR: Visual Studio 12 2013 - image: Visual Studio 2017 @@ -72,13 +63,26 @@ matrix: GENERATOR: Visual Studio 14 2015 - image: Visual Studio 2017 GENERATOR: Visual Studio 14 2015 Win64 + - image: Visual Studio 2017 + GENERATOR: Visual Studio 16 2019 + - image: Visual Studio 2019 + GENERATOR: Visual Studio 12 2013 + - image: Visual Studio 2019 + GENERATOR: Visual Studio 12 2013 Win64 + - image: Visual Studio 2019 + GENERATOR: Visual Studio 14 2015 + - image: Visual Studio 2019 + GENERATOR: Visual Studio 14 2015 Win64 + - image: Visual Studio 2019 + GENERATOR: Visual Studio 15 2017 + - image: Visual Studio 2019 + GENERATOR: Visual Studio 15 2017 Win64 install: - git submodule update --init --recursive - - set PATH=%PATH%;C:\Qt\%QT_VERSION%\%QT%\bin build_script: - - python ci/configure.py -q -a %AUDIO_DRIVER% -g "%GENERATOR%" + - python ci/configure.py -a %AUDIO_DRIVER% -g "%GENERATOR%" - python ci/build.py --configuration %CONFIGURATION% test_script: diff --git a/.travis.yml b/.travis.yml index 87550d4..bfb93cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: cpp sudo: required -dist: trusty branches: only: - master @@ -38,6 +37,7 @@ matrix: # Linux with Clang 3.6, 64-bit Debug, Alsa - os: linux + dist: trusty compiler: clang addons: apt: @@ -45,38 +45,29 @@ matrix: packages: ['clang-3.6', 'g++-5', 'valgrind', 'portaudio19-dev'] env: COMPILER=clang++-3.6 WORDSIZE=64 CONFIGURATION=Debug AUDIO=Alsa - # Linux with Clang 3.6, 64-bit Release, Jack + # Linux with Clang 3.8, 64-bit Release, Jack - os: linux + dist: trusty compiler: clang addons: apt: - sources: ['llvm-toolchain-precise-3.8', 'ubuntu-toolchain-r-test'] + sources: ['ubuntu-toolchain-r-test'] packages: ['clang-3.8', 'g++-5', 'valgrind', 'portaudio19-dev'] env: COMPILER=clang++-3.8 WORDSIZE=64 CONFIGURATION=Release AUDIO=Jack # Linux with Clang 5.0, 64-bit Debug, Alsa - os: linux + dist: bionic compiler: clang addons: apt: - sources: ['llvm-toolchain-trusty-5.0', 'ubuntu-toolchain-r-test'] + sources: ['ubuntu-toolchain-r-test'] packages: ['clang-5.0', 'g++-5', 'valgrind', 'portaudio19-dev'] env: COMPILER=clang++-5.0 WORDSIZE=64 CONFIGURATION=Debug AUDIO=Alsa - # Linux with GCC 5.x, 32-bit Release -# - os: linux -# compiler: gcc -# addons: -# apt: -# sources: ['ubuntu-toolchain-r-test'] -# packages: ['g++-5', 'g++-5-multilib', 'linux-libc-dev:i386', 'valgrind:i386', -# 'libxext-dev:i386', 'libglapi-mesa:i386', -# 'libgl1-mesa-glx:i386', 'libgl1-mesa-dev:i386', -# 'portaudio19-dev:i386', 'libglib2.0-0:i386'] -# env: COMPILER=g++-5 WORDSIZE=32 CONFIGURATION=Release - # Linux with GCC 5.x, 64-bit Release, Alsa - os: linux + dist: bionic compiler: gcc addons: apt: @@ -86,6 +77,7 @@ matrix: # Linux with GCC 6.x, 64-bit Release, Alsa - os: linux + dist: bionic compiler: gcc addons: apt: @@ -93,8 +85,19 @@ matrix: packages: ['g++-6', 'valgrind', 'portaudio19-dev'] env: COMPILER=g++-6 WORDSIZE=64 CONFIGURATION=Release AUDIO=Alsa + # ESP32 + - os: linux + dist: bionic + compiler: clang + addons: + apt: + sources: ['llvm-toolchain-trusty-5.0', 'ubuntu-toolchain-r-test'] + packages: ['wget', 'flex', 'bison', 'gperf', 'python-pip', 'ninja-build', 'ccache'] + env: IDF_RELEASE=v4.0-rc IDF_PATH=${HOME}/esp-idf-${IDF_RELEASE} COMPILER=clang + # Code formatting verification - os: linux + dist: bionic compiler: clang env: CONFIGURATION=Formatting LLVM_VERSION=6.0 services: @@ -112,40 +115,38 @@ before_install: # Override Travis' CXX Flag - CXX=$COMPILER - # Install homebrew packages for Mac OS X - - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update && brew install ninja qt5; fi - - # Install QT for Linux - - | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then - sudo apt-add-repository -y ppa:beineri/opt-qt-5.10.1-trusty - sudo apt-get -qy update - sudo apt-get -qy install qtdeclarative5-dev - fi - install: - git submodule update --init --recursive script: - | set -e - if [ "$TRAVIS_OS_NAME" = "osx" ]; then - PATH=`brew --prefix qt5`/bin:${PATH} - if [ "$WORDSIZE" -eq 64 ]; then - python ci/configure.py --configuration $CONFIGURATION --generator Ninja --with-qt - else - python ci/configure.py --configuration $CONFIGURATION --generator Ninja --flags "\-DCMAKE_OSX_ARCHITECTURES=i386" - fi - python ci/build.py --configuration $CONFIGURATION --arguments "all -v" + if [ ! -z "$IDF_PATH" ]; then + mkdir -p ${IDF_PATH} + wget -P ${HOME} https://github.com/espressif/esp-idf/releases/download/${IDF_RELEASE}/esp-idf-${IDF_RELEASE}.zip + unzip -o ${HOME}/esp-idf-${IDF_RELEASE}.zip -d ${HOME} + ${IDF_PATH}/install.sh + . ${IDF_PATH}/export.sh + . ${IDF_PATH}/add_path.sh + PATH=${IDF_PATH}/tools:${PATH} + cd examples/esp32 + idf.py build else - if [ "$WORDSIZE" -eq 64 ]; then - PATH=$PWD/5.5/gcc_64:${PATH} - python ci/configure.py --configuration $CONFIGURATION --with-qt -a $AUDIO + if [ "$TRAVIS_OS_NAME" = "osx" ]; then + if [ "$WORDSIZE" -eq 64 ]; then + python ci/configure.py --configuration $CONFIGURATION + else + python ci/configure.py --configuration $CONFIGURATION --flags "\-DCMAKE_OSX_ARCHITECTURES=i386" + fi + python ci/build.py --configuration $CONFIGURATION else - PATH=$PWD/5.5/gcc:${PATH} - python ci/configure.py --configuration $CONFIGURATION -a $AUDIO --flags "\-DCMAKE_CXX_FLAGS=\"\-m32\"" + if [ "$WORDSIZE" -eq 64 ]; then + python ci/configure.py --configuration $CONFIGURATION -a $AUDIO + else + python ci/configure.py --configuration $CONFIGURATION -a $AUDIO --flags "\-DCMAKE_CXX_FLAGS=\"\-m32\"" + fi + python ci/build.py --configuration $CONFIGURATION fi - python ci/build.py --configuration $CONFIGURATION --arguments "VERBOSE=1 -j8" fi set +e @@ -154,11 +155,13 @@ script: # compiled by brew and this takes way too much time on the build server. - | set -e - if [ "$TRAVIS_OS_NAME" = "linux" ]; then - python ci/run-tests.py --target LinkCoreTest --valgrind - python ci/run-tests.py --target LinkDiscoveryTest --valgrind - else - python ci/run-tests.py --target LinkCoreTest - python ci/run-tests.py --target LinkDiscoveryTest + if [ -z "$IDF_PATH" ]; then + if [ "$TRAVIS_OS_NAME" = "linux" ]; then + python ci/run-tests.py --target LinkCoreTest --valgrind + python ci/run-tests.py --target LinkDiscoveryTest --valgrind + else + python ci/run-tests.py --target LinkCoreTest + python ci/run-tests.py --target LinkDiscoveryTest + fi fi set +e diff --git a/AbletonLinkConfig.cmake b/AbletonLinkConfig.cmake index ce77a6d..c34d8e3 100644 --- a/AbletonLinkConfig.cmake +++ b/AbletonLinkConfig.cmake @@ -31,10 +31,6 @@ elseif(WIN32) INTERFACE_COMPILE_DEFINITIONS LINK_PLATFORM_WINDOWS=1 ) - set_property(TARGET Ableton::Link APPEND PROPERTY - INTERFACE_COMPILE_OPTIONS - "/wd4503" # 'Identifier': decorated name length exceeded, name was truncated - ) elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set_property(TARGET Ableton::Link APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS diff --git a/CMakeLists.txt b/CMakeLists.txt index d47b1c8..100fe2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # |_| # Note: Please use the LINK_* prefix for all project-specific options -option(LINK_BUILD_QT_EXAMPLES "Build examples (Requires Qt)" OFF) if(UNIX) option(LINK_ENABLE_ASAN "Build with Address Sanitizier (ASan)" OFF) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53764dc..c4a0fd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,18 +22,18 @@ the following criteria: - You have signed and returned Ableton's [CLA][cla] - The [tests pass](#testing) - - The PR passes all [CI service checks](#ci-services) + - The PR passes all CI service checks - The code is [well-formatted](#code-formatting) - The git commit messages comply to [the commonly accepted standards][git-commit-msgs] Testing ------- -Link ships with unit tests that are run by our [CI services](#ci-services) for all PRs. -There are two test suites: `LinkCoreTest`, which tests the core Link functionality, and -`LinkDiscoverTest`, which tests the network discovery feature of Link. A third virtual -target, `LinkAllTest` is provided by the CMake project as a convenience to run all tests -at once. +Link ships with unit tests that are run on [Travis CI][travis] and [AppVeyor][appveyor] for +all PRs. There are two test suites: `LinkCoreTest`, which tests the core Link +functionality, and `LinkDiscoverTest`, which tests the network discovery feature of Link. +A third virtual target, `LinkAllTest` is provided by the CMake project as a convenience +to run all tests at once. The unit tests are run on every platform which Link is officially supported on, and also are run through [Valgrind][valgrind] on Linux to check for memory corruption and leaks. If @@ -42,31 +42,11 @@ valgrind detects any memory errors when running the tests, it will fail the buil If you are submitting a PR which fixes a bug or introduces new functionality, please add a test which helps to verify the correctness of the code in the PR. - -CI Services ------------ - -Every PR submitted to Link must build and pass tests on the following platforms - -| Service | Platform | Compiler | Word size | Checks | -| -------- | -------- | --------- | ----------| ------ | -| AppVeyor | Windows | MSVC 2013 | 32/64-bit | Build (Release), run tests | -| AppVeyor | Windows | MSVC 2015 | 32/64-bit | Build (Debug/Release), run tests | -| Travis | Mac OS X | Xcode 7.1 | 64-bit | Build (Debug/Release), run tests | -| Travis | Linux | Clang-3.6 | 64-bit | Build (Debug/Release), run tests with Valgrind | -| Travis | Linux | GCC-5.2 | 32/64-bit | Build (Release), run tests with Valgrind | -| Travis | Linux | Clang-3.8 | N/A | Check code formatting with clang-format | - -Assuming that your PR gets a green checkmark from each service, you're good to go. In case -of a build failure, please examine the build logs from the respective CI service. If you -have any questions, feel free to contact one of the maintainers -- we're here to help you! - - Code Formatting --------------- Link uses [clang-format][clang-format] to enforce our preferred code style. At the moment, -we use **clang-format version 3.9**. Note that other versions may format code differently. +we use **clang-format version 6.0**. Note that other versions may format code differently. Any PRs submitted to Link are also checked with clang-format by the Travis CI service. If you get a build failure, then you can format your code by running the following command: @@ -79,3 +59,5 @@ clang-format -style=file -i (filename) [git-commit-msgs]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [clang-format]: http://llvm.org/builds [valgrind]: http://valgrind.org +[travis]: http://travis-ci.com +[appveyor]: http://appveyor.com @@ -32,28 +32,8 @@ $ cmake .. $ cmake --build . ``` -In order to build the GUI example application **QLinkHut**, the [Qt][qt] installation -path must be set in the system PATH and `LINK_BUILD_QT_EXAMPLES` must be set: - -``` -$ mkdir build -$ cd build -$ cmake -DLINK_BUILD_QT_EXAMPLES=ON .. -$ cmake --build . -``` - The output binaries for the example applications and the unit-tests will be placed in a -`bin` subdirectory of the CMake binary directory. Also note that the word size of the Qt -installation must match how Link has been configured. Look for the value of -`LINK_WORD_SIZE` in the CMake output to verify that the word size matches Qt's. - -When running QLinkHut on Windows, the Qt binary path must be in the system `PATH` before -launching the executable. So to launch QLinkHut from Visual Studio, one must go to the -QLinkHut Properties -> Debugging -> Environment, and set it to: - -``` -PATH=$(Path);C:\path\to\Qt\5.5\msvc64_bin\bin -``` +`bin` subdirectory of the CMake binary directory. # Integrating Link in your Application @@ -100,7 +80,6 @@ insight as to the compiler flags needed to build Link. | Platform | Minimum Required | Optional (only required for examples) | |----------|----------------------|---------------------------------------| -| **All** | CMake 3.0 | Qt 5.5 | | Windows | MSVC 2013 | Steinberg ASIO SDK 2.3 | | Mac | Xcode 7.0 | | | Linux | Clang 3.6 or GCC 5.2 | libportaudio19-dev | @@ -115,8 +94,8 @@ information on the LinkKit SDK for iOS. An overview of Link concepts can be found at http://ableton.github.io/link. Those that are new to Link should start there. The [Link.hpp](include/ableton/Link.hpp) header -contains the full Link public interface. See the LinkHut and QLinkHut projects in this -repo for an example usage of the `Link` type. +contains the full Link public interface. See the LinkHut projects in this repo for an +example usage of the `Link` type. ## Time and Clocks @@ -175,4 +154,3 @@ example apps. [catch]: https://github.com/philsquared/Catch [cmake]: https://www.cmake.org [license]: LICENSE.md -[qt]: https://www.qt.io diff --git a/TEST-PLAN.md b/TEST-PLAN.md index 39f82e4..3b00d61 100644 --- a/TEST-PLAN.md +++ b/TEST-PLAN.md @@ -6,59 +6,80 @@ behave consistently with respect to these test cases. ## Tempo Changes ### TEMPO-1: Tempo changes should be transmitted between connected apps. -- Open LinkHut, press **Play**, when using QLinkHut click the **Link** button to enable -Link. - Open App and **enable** Link. - Without starting to play, change tempo in App -**⇒** LinkHut clicks should speed up or slow down to match the tempo specified in -the App. - Start playing in the App **⇒** App and LinkHut should be in sync - Change -tempo in App and in LinkHut **⇒** App and LinkHut should remain in sync +- Open LinkHut, press **Play** and **enable** Link. +- Open App and **enable** Link. +- Without starting to play, change tempo in App **⇒** LinkHut clicks should speed up or slow down to match the tempo specified in +the App. +- Start playing in the App **⇒** App and LinkHut should be in sync +- Change tempo in App and in LinkHut **⇒** App and LinkHut should remain in sync ### TEMPO-2: Opening an app with Link enabled should not change the tempo of an existing Link session. -- Open App and **enable** Link. - Set App tempo to 100bpm. - Terminate App. - Open -LinkHut, press **Play** and **enable** Link. - Set LinkHut tempo to 130bpm. - Open App -and **enable** Link. **⇒** Link should be connected (“1 Link”) and the App and +- Open App and **enable** Link. +- Set App tempo to 100bpm. +- Terminate App. +- Open LinkHut, press **Play** and **enable** Link. +- Set LinkHut tempo to 130bpm. +- Open App and **enable** Link. **⇒** Link should be connected (“1 Link”) and the App and LinkHut’s tempo should both be 130bpm. ### TEMPO-3: When connected, loading a new document should not change the Link session tempo. -- Open LinkHut, press **Play**, when using QLinkHut click the **Link** button to enable -Link. - Set LinkHut tempo to 130bpm. - Open App and **enable** Link **⇒** LinkHut’s -tempo should not change. - Load new Song/Set/Session with a tempo other than 130bpm -**⇒** App and LinkHut tempo should both be 130bpm. +- Open LinkHut, press **Play** and **enable** Link. +- Set LinkHut tempo to 130bpm. +- Open App and **enable** Link **⇒** LinkHut’s tempo should not change. +- Load new Song/Set/Session with a tempo other than 130bpm **⇒** App and LinkHut tempo should both be 130bpm. ### TEMPO-4: Tempo range handling. -- Open LinkHut, press **Play**, when using QLinkHut click the **Link** button to enable -Link. - Open App, start Audio, and **enable** Link. - Change tempo in LinkHut to -**20bpm** **⇒** App and LinkHut should stay in sync. - Change Tempo in LinkHut to -**999bpm** **⇒** App and LinkHut should stay in sync. - If App does not support the -full range of tempos supported by Link, it should stay in sync by switching to a multiple -of the Link session tempo. +- Open LinkHut, press **Play**, enable Link. +- Open App, start Audio, and **enable** Link. +- Change tempo in LinkHut to **20bpm** **⇒** App and LinkHut should stay in sync. +- Change Tempo in LinkHut to **999bpm** **⇒** App and LinkHut should stay in sync. +- If App does not support the full range of tempos supported by Link, it should stay in sync by switching to a multiple of the Link session tempo. ### TEMPO-5: Enabling Link does not change app's tempo if there is no Link session to join. -- Open App, start playing. - Change App tempo to something other than the default. -- **Enable** Link **⇒** App's tempo should not change. - Change App tempo to a new -value (not the default). - **Disable** Link **⇒** App's tempo should not change. +- Open App, start playing. +- Change App tempo to something other than the default. +- **Enable** Link **⇒** App's tempo should not change. +- Change App tempo to a new value (not the default). +- **Disable** Link **⇒** App's tempo should not change. ## Beat Time These cases verify the continuity of beat time across Link operations. ### BEATTIME-1: Enabling Link does not change app's beat time if there is no Link session to join. -- Open App, start playing. - **Enable** Link **⇒** No beat time jump or -audible discontinuity should occur. - **Disable** Link **⇒** No beat time jump or -audible discontinuity should occur. +- Open App, start playing. +- **Enable** Link **⇒** No beat time jump or audible discontinuity should occur. +- **Disable** Link **⇒** No beat time jump or audible discontinuity should occur. ### BEATTIME-2: App's beat time does not change if another participant joins its session. -- Open App and **enable** Link. - Start playing. - Open LinkHut, whenusing QLinkHut -**enable** Link **⇒** No beat time jump or audible discontinuity should occur in the -App. +- Open App and **enable** Link. +- Start playing. +- Open LinkHut and **enable** Link **⇒** No beat time jump or audible discontinuity should occur in the App. **Note**: When joining an existing Link session, an app should adjust to the existing session's tempo and phase, which will usually result in a beat time jump. Apps that are already in a session should never have any kind of beat time or audio discontinuity when a new participant joins the session. +## Start Stop States + +### STARTSTOPSTATE-1: Listening to start/stop commands from other peers. +- Open App, set Link and Start Stop Sync to **Enabled**. +- Open LinkHut, **enable** Link and Start Stop Sync and press **Play** **⇒** App should start playing according to its quantization. +- Stop playback in LinkHut **⇒** App should stop playing. + +### STARTSTOPSTATE-2: Sending start/stop commands to other peers. +- Open LinkHut, **enable** Link and Start Stop Sync and press **Play**. +- Open App, set Link and Start Stop Sync to **Enabled** **⇒** App should not be +playing while LinkHut continues playing. +- Start playback in App **⇒** App should join playing according to its quantization. +- Stop playback in App **⇒** App and LinkHut should stop playing. +- Start playback in App **⇒** App and LinkHut should start playing according to +their quantizations. + ## Audio Engine These cases verify the correct implementation of latency compensation within an app's @@ -66,10 +87,11 @@ audio engine. ### AUDIOENGINE-1: Correct alignment of app audio with shared session - - Connect the audio out of your computer to the audio in. Alternatively use +- Connect the audio out of your computer to the audio in. Alternatively use [SoundFlower](https://github.com/mattingalls/Soundflower) to be able to record the output -of your app and LinkHut. - Open LinkHut, press **Play**, , when using QLinkHut click the -**Link** button to enable Link. - Open App and **enable** Link. - Start playing audio -(preferably a short, click-like sample) with notes on the same beats as LinkHut. - Record -audio within application of choice. - Validate whether onset of the sample aligns with -the pulse generated by LinkHut (tolerance: less than 3 ms). +of your app and LinkHut. +- Open LinkHut, **enable** Link and press **Play**. +- Open App and **enable** Link. +- Start playing audio (preferably a short, click-like sample) with notes on the same beats as LinkHut. +- Record audio within application of choice. +- Validate whether onset of the sample aligns with the pulse generated by LinkHut (tolerance: less than 3 ms). diff --git a/ci/configure.py b/ci/configure.py index b3a4a2f..451e2f3 100755 --- a/ci/configure.py +++ b/ci/configure.py @@ -27,11 +27,6 @@ def parse_args(): help='Build configuration to use (not supported by IDE generators)') arg_parser.add_argument( - '-q', '--with-qt', - help='Build Qt example apps', - action='store_true') - - arg_parser.add_argument( '-g', '--generator', help='CMake generator to use (default: Determined by CMake)') @@ -54,9 +49,6 @@ def build_cmake_args(args): cmake_args.append('-G') cmake_args.append(args.generator) - if args.with_qt: - cmake_args.append('-DLINK_BUILD_QT_EXAMPLES=ON') - if args.configuration is not None: cmake_args.append('-DCMAKE_BUILD_TYPE=' + args.configuration) diff --git a/cmake_include/ConfigureCompileFlags.cmake b/cmake_include/ConfigureCompileFlags.cmake index 96ceb03..4cf5140 100644 --- a/cmake_include/ConfigureCompileFlags.cmake +++ b/cmake_include/ConfigureCompileFlags.cmake @@ -107,8 +107,12 @@ elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) "/wd4640" # 'Instance': construction of local static object is not thread-safe "/wd4710" # 'Function': function not inlined "/wd4711" # Function 'function' selected for inline expansion + "/wd4723" # potential divide by 0 "/wd4738" # Storing 32-bit float result in memory, possible loss of performance "/wd4820" # 'Bytes': bytes padding added after construct 'member_name' + "/wd4996" # Your code uses a function, class member, variable, or typedef that's marked deprecated + "/wd5045" # Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified + "/wd5204" # 'class' : class has virtual functions, but destructor is not virtual ) if(MSVC_VERSION VERSION_GREATER 1800) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 1cafbae..b85107f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -103,24 +103,6 @@ function(configure_linkhut_executable target) target_link_libraries(${target} Ableton::Link) endfunction() -function(configure_qlinkhut_executable target) - configure_linkhut_executable(${target}) - target_link_libraries(${target} Qt5::Quick) - - if(${CMAKE_CXX_COMPILER_ID} MATCHES Clang) - target_compile_options(${target} PRIVATE - "-Wno-global-constructors" - "-Wno-missing-prototypes" - "-Wno-undefined-reinterpret-cast" - "-Wno-zero-as-null-pointer-constant" - ) - endif() - - if(MSVC_VERSION VERSION_GREATER 1900) - target_compile_options(${target} PRIVATE "/wd4946") - endif() -endfunction() - function(configure_linkhut_audio_sources target) if(APPLE) target_link_libraries(${target} "-framework AudioUnit") @@ -210,66 +192,32 @@ configure_linkhut_audio_sources(LinkHut) configure_linkhut_executable(LinkHut) source_group("LinkHut" FILES ${linkhut_HEADERS} ${linkhut_SOURCES}) -# ___ _ _ _ _ _ _ -# / _ \| | (_)_ __ | | _| | | |_ _| |_ -# | | | | | | | '_ \| |/ / |_| | | | | __| -# | |_| | |___| | | | | <| _ | |_| | |_ -# \__\_\_____|_|_| |_|_|\_\_| |_|\__,_|\__| +# _ _ _ _ _ _ ____ _ _ _ +# | | (_)_ __ | | _| | | |_ _| |_/ ___|(_) | ___ _ __ | |_ +# | | | | '_ \| |/ / |_| | | | | __\___ \| | |/ _ \ '_ \| __| +# | |___| | | | | <| _ | |_| | |_ ___) | | | __/ | | | |_ +# |_____|_|_| |_|_|\_\_| |_|\__,_|\__|____/|_|_|\___|_| |_|\__| # -if(LINK_BUILD_QT_EXAMPLES) - - set(CMAKE_AUTOMOC ON) - set(CMAKE_INCLUDE_CURRENT_DIR ON) - - find_package(Qt5Quick REQUIRED) - - set(qlinkhut_HEADERS - linkaudio/AudioEngine.hpp - linkaudio/AudioPlatform.hpp - qlinkhut/Controller.hpp - ${link_HEADERS} - ) - - set(qlinkhut_SOURCES - linkaudio/AudioEngine.cpp - qlinkhut/Controller.cpp - qlinkhut/main.cpp - ${link_SOURCES} - ) - source_group("QLinkHut" FILES ${qlinkhut_HEADERS} ${qlinkhut_SOURCES}) - qt5_add_resources(UI_RESOURCES qlinkhut/resources.qrc) - - add_executable(QLinkHut - ${qlinkhut_HEADERS} - ${qlinkhut_SOURCES} - ${linkhut_audio_SOURCES} - ${UI_RESOURCES} - ) - - configure_linkhut_audio_sources(QLinkHut) - configure_qlinkhut_executable(QLinkHut) +set(linkhutsilent_HEADERS + linkaudio/AudioEngine.hpp + linkaudio/AudioPlatform_Dummy.hpp + ${link_HEADERS} +) - # A silent version of QLinkHut that uses the Link application context instead of - # the audio buffer context. It is the same as QLinkHut except that it doesn't - # generate sound. +set(linkhutsilent_SOURCES + linkaudio/AudioEngine.cpp + linkhut/main.cpp +) - add_executable(QLinkHutSilent - ${qlinkhut_HEADERS} - ${qlinkhut_SOURCES} - linkaudio/AudioPlatform_Dummy.hpp - ${UI_RESOURCES} - ) +add_executable(LinkHutSilent + ${linkhutsilent_HEADERS} + ${linkhutsilent_SOURCES} +) - target_compile_definitions(QLinkHutSilent PRIVATE - -DLINKHUT_AUDIO_PLATFORM_DUMMY=1 - ) - if(UNIX) - set_target_properties(QLinkHutSilent - PROPERTIES - COMPILE_FLAGS "-Wno-unused" - ) - endif() +target_compile_definitions(LinkHutSilent PRIVATE + -DLINKHUT_AUDIO_PLATFORM_DUMMY=1 +) - configure_qlinkhut_executable(QLinkHutSilent) -endif() +configure_linkhut_executable(LinkHutSilent) +source_group("LinkHutSilent" FILES ${linkhutsilent_HEADERS} ${linkhutsilent_SOURCES}) diff --git a/examples/esp32/CMakeLists.txt b/examples/esp32/CMakeLists.txt new file mode 100644 index 0000000..b6d956d --- /dev/null +++ b/examples/esp32/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.5) + +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(link_esp32_example)
\ No newline at end of file diff --git a/examples/esp32/README.md b/examples/esp32/README.md new file mode 100644 index 0000000..9647af4 --- /dev/null +++ b/examples/esp32/README.md @@ -0,0 +1,12 @@ +# *E X P E R I M E N T A L* + +*Tested with esp-idf [v4.0-beta2](https://github.com/espressif/esp-idf/releases/tag/v4.0-rc)* + +## Building and Running the Example + +* Setup esp-idf as described in [the documentation](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) +* Run `idf.py menuconfig` and setup WiFi credentials under +`Example Connection Configuration` +``` +idf.py build +idf.py -p ${ESP32_SERIAL_PORT} flash diff --git a/examples/esp32/main/CMakeLists.txt b/examples/esp32/main/CMakeLists.txt new file mode 100644 index 0000000..64756ee --- /dev/null +++ b/examples/esp32/main/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_component_register(SRCS main.cpp) + +if(NOT DEFINED LINK_ESP_TASK_CORE_ID) + set(LINK_ESP_TASK_CORE_ID tskNO_AFFINITY) +endif() +target_compile_definitions(${COMPONENT_LIB} PRIVATE LINK_ESP_TASK_CORE_ID=${LINK_ESP_TASK_CORE_ID}) + +target_compile_options(${COMPONENT_LIB} PRIVATE -fexceptions) + +include(../../../AbletonLinkConfig.cmake) +target_link_libraries(${COMPONENT_TARGET} Ableton::Link) diff --git a/examples/esp32/main/main.cpp b/examples/esp32/main/main.cpp new file mode 100644 index 0000000..6c8892f --- /dev/null +++ b/examples/esp32/main/main.cpp @@ -0,0 +1,108 @@ +#include <ableton/Link.hpp> +#include <driver/gpio.h> +#include <driver/timer.h> +#include <esp_event.h> +#include <freertos/FreeRTOS.h> +#include <freertos/semphr.h> +#include <freertos/task.h> +#include <nvs_flash.h> +#include <protocol_examples_common.h> + +#define LED GPIO_NUM_2 +#define PRINT_LINK_STATE false + +unsigned int if_nametoindex(const char* ifName) +{ + return 0; +} + +char* if_indextoname(unsigned int ifIndex, char* ifName) +{ + return nullptr; +} + +void IRAM_ATTR timer_group0_isr(void* userParam) +{ + static BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + TIMERG0.int_clr_timers.t0 = 1; + TIMERG0.hw_timer[0].config.alarm_en = 1; + + xSemaphoreGiveFromISR(userParam, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken) + { + portYIELD_FROM_ISR(); + } +} + +void timerGroup0Init(int timerPeriodUS, void* userParam) +{ + timer_config_t config = {.alarm_en = TIMER_ALARM_EN, + .counter_en = TIMER_PAUSE, + .intr_type = TIMER_INTR_LEVEL, + .counter_dir = TIMER_COUNT_UP, + .auto_reload = TIMER_AUTORELOAD_EN, + .divider = 80}; + + timer_init(TIMER_GROUP_0, TIMER_0, &config); + timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0); + timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, timerPeriodUS); + timer_enable_intr(TIMER_GROUP_0, TIMER_0); + timer_isr_register(TIMER_GROUP_0, TIMER_0, &timer_group0_isr, userParam, 0, nullptr); + + timer_start(TIMER_GROUP_0, TIMER_0); +} + +void printTask(void* userParam) +{ + auto link = static_cast<ableton::Link*>(userParam); + const auto quantum = 4.0; + + while (true) + { + const auto sessionState = link->captureAppSessionState(); + const auto numPeers = link->numPeers(); + const auto time = link->clock().micros(); + const auto beats = sessionState.beatAtTime(time, quantum); + std::cout << std::defaultfloat << "| peers: " << numPeers << " | " + << "tempo: " << sessionState.tempo() << " | " << std::fixed + << "beats: " << beats << " |" << std::endl; + vTaskDelay(800 / portTICK_PERIOD_MS); + } +} + +void tickTask(void* userParam) +{ + ableton::Link link(120.0f); + link.enable(true); + + if (PRINT_LINK_STATE) + { + xTaskCreate(printTask, "print", 8192, &link, 1, nullptr); + } + + gpio_set_direction(LED, GPIO_MODE_OUTPUT); + + while (true) + { + xSemaphoreTake(userParam, portMAX_DELAY); + + const auto state = link.captureAudioSessionState(); + const auto phase = state.phaseAtTime(link.clock().micros(), 1.); + gpio_set_level(LED, fmodf(phase, 1.) < 0.1); + portYIELD(); + } +} + +extern "C" void app_main() +{ + ESP_ERROR_CHECK(nvs_flash_init()); + tcpip_adapter_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(example_connect()); + + SemaphoreHandle_t tickSemphr = xSemaphoreCreateBinary(); + timerGroup0Init(100, tickSemphr); + + xTaskCreate(tickTask, "tick", 8192, tickSemphr, configMAX_PRIORITIES - 1, nullptr); +} diff --git a/examples/esp32/sdkconfig.defaults b/examples/esp32/sdkconfig.defaults new file mode 100644 index 0000000..75ebeae --- /dev/null +++ b/examples/esp32/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y diff --git a/examples/linkaudio/AudioEngine.cpp b/examples/linkaudio/AudioEngine.cpp index f2802d0..323d634 100644 --- a/examples/linkaudio/AudioEngine.cpp +++ b/examples/linkaudio/AudioEngine.cpp @@ -24,6 +24,7 @@ #define _USE_MATH_DEFINES #endif #include <cmath> +#include <iostream> namespace ableton { @@ -33,12 +34,16 @@ namespace linkaudio AudioEngine::AudioEngine(Link& link) : mLink(link) , mSampleRate(44100.) - , mOutputLatency(0) + , mOutputLatency(std::chrono::microseconds{0}) , mSharedEngineData({0., false, false, 4., false}) , mLockfreeEngineData(mSharedEngineData) , mTimeAtLastClick{} , mIsPlaying(false) { + if (!mOutputLatency.is_lock_free()) + { + std::cout << "WARNING: AudioEngine::mOutputLatency is not lock free!" << std::endl; + } } void AudioEngine::startPlaying() @@ -143,7 +148,7 @@ void AudioEngine::renderMetronomeIntoBuffer(const Link::SessionState sessionStat { double amplitude = 0.; // Compute the host time for this sample and the last. - const auto hostTime = beginHostTime + microseconds(llround(i * microsPerSample)); + const auto hostTime = beginHostTime + microseconds(llround(static_cast<double>(i) * microsPerSample)); const auto lastSampleHostTime = hostTime - microseconds(llround(microsPerSample)); // Only make sound for positive beat magnitudes. Negative beat diff --git a/examples/linkaudio/AudioEngine.hpp b/examples/linkaudio/AudioEngine.hpp index 5225ffd..f9c94d5 100644 --- a/examples/linkaudio/AudioEngine.hpp +++ b/examples/linkaudio/AudioEngine.hpp @@ -22,6 +22,7 @@ // Make sure to define this before <cmath> is included for Windows #define _USE_MATH_DEFINES #include <ableton/Link.hpp> +#include <atomic> #include <mutex> namespace ableton @@ -64,7 +65,7 @@ private: Link& mLink; double mSampleRate; - std::chrono::microseconds mOutputLatency; + std::atomic<std::chrono::microseconds> mOutputLatency; std::vector<double> mBuffer; EngineData mSharedEngineData; EngineData mLockfreeEngineData; diff --git a/examples/linkaudio/AudioPlatform_Asio.cpp b/examples/linkaudio/AudioPlatform_Asio.cpp index fb4250d..40aa20a 100644 --- a/examples/linkaudio/AudioPlatform_Asio.cpp +++ b/examples/linkaudio/AudioPlatform_Asio.cpp @@ -112,7 +112,7 @@ void AudioPlatform::audioCallback(ASIOTime* timeInfo, long index) asioSamplesToDouble(timeInfo->timeInfo.samplePosition)); } - const auto bufferBeginAtOutput = hostTime + mEngine.mOutputLatency; + const auto bufferBeginAtOutput = hostTime + mEngine.mOutputLatency.load(); ASIOBufferInfo* bufferInfos = mDriverInfo.bufferInfos; const long numSamples = mDriverInfo.preferredSize; @@ -220,12 +220,13 @@ void AudioPlatform::createAsioBuffers() << ", output latency: " << outputLatency << "usec" << std::endl; const double bufferSize = driverInfo.preferredSize / driverInfo.sampleRate; - mEngine.mOutputLatency = + auto outputLatencyMicros = std::chrono::microseconds(llround(outputLatency / driverInfo.sampleRate)); - mEngine.mOutputLatency += std::chrono::microseconds(llround(1.0e6 * bufferSize)); + outputLatencyMicros += std::chrono::microseconds(llround(1.0e6 * bufferSize)); + + mEngine.mOutputLatency.store(outputLatencyMicros); - const auto totalLatency = mEngine.mOutputLatency.count(); - std::clog << "Total latency: " << totalLatency << "usec" << std::endl; + std::clog << "Total latency: " << outputLatencyMicros.count() << "usec" << std::endl; } void AudioPlatform::initializeDriverInfo() diff --git a/examples/linkaudio/AudioPlatform_CoreAudio.cpp b/examples/linkaudio/AudioPlatform_CoreAudio.cpp index 5574bc1..8ec07ed 100644 --- a/examples/linkaudio/AudioPlatform_CoreAudio.cpp +++ b/examples/linkaudio/AudioPlatform_CoreAudio.cpp @@ -50,7 +50,7 @@ OSStatus AudioPlatform::audioCallback(void* inRefCon, AudioEngine* engine = static_cast<AudioEngine*>(inRefCon); const auto bufferBeginAtOutput = - engine->mLink.clock().ticksToMicros(inTimeStamp->mHostTime) + engine->mOutputLatency; + engine->mLink.clock().ticksToMicros(inTimeStamp->mHostTime) + engine->mOutputLatency.load(); engine->audioCallback(bufferBeginAtOutput, inNumberFrames); @@ -161,7 +161,7 @@ void AudioPlatform::initialize() using namespace std::chrono; const double latency = static_cast<double>(deviceLatency) / mEngine.mSampleRate; - mEngine.mOutputLatency = duration_cast<microseconds>(duration<double>{latency}); + mEngine.mOutputLatency.store(duration_cast<microseconds>(duration<double>{latency})); AURenderCallbackStruct ioRemoteInput; ioRemoteInput.inputProc = audioCallback; diff --git a/examples/linkaudio/AudioPlatform_Dummy.hpp b/examples/linkaudio/AudioPlatform_Dummy.hpp index bd9905d..b5ff745 100644 --- a/examples/linkaudio/AudioPlatform_Dummy.hpp +++ b/examples/linkaudio/AudioPlatform_Dummy.hpp @@ -19,7 +19,7 @@ #pragma once -#include "AudioEngine.hpp" +#include <ableton/Link.hpp> namespace ableton { @@ -28,6 +28,77 @@ namespace linkaudio class AudioPlatform { + class AudioEngine + { + public: + AudioEngine(Link& link) + : mLink(link) + , mQuantum(4.) + { + } + + void startPlaying() + { + auto sessionState = mLink.captureAppSessionState(); + sessionState.setIsPlayingAndRequestBeatAtTime(true, now(), 0., mQuantum); + mLink.commitAppSessionState(sessionState); + } + + void stopPlaying() + { + auto sessionState = mLink.captureAppSessionState(); + sessionState.setIsPlaying(false, now()); + mLink.commitAppSessionState(sessionState); + } + + bool isPlaying() const + { + return mLink.captureAppSessionState().isPlaying(); + } + + double beatTime() const + { + auto sessionState = mLink.captureAppSessionState(); + return sessionState.beatAtTime(now(), mQuantum); + } + + void setTempo(double tempo) + { + auto sessionState = mLink.captureAppSessionState(); + sessionState.setTempo(tempo, now()); + mLink.commitAppSessionState(sessionState); + } + + double quantum() const + { + return mQuantum; + } + + void setQuantum(double quantum) + { + mQuantum = quantum; + } + + bool isStartStopSyncEnabled() const + { + return mLink.isStartStopSyncEnabled(); + } + + void setStartStopSyncEnabled(bool enabled) + { + mLink.enableStartStopSync(enabled); + } + + private: + std::chrono::microseconds now() const + { + return mLink.clock().micros(); + } + + Link& mLink; + double mQuantum; + }; + public: AudioPlatform(Link& link) : mEngine(link) diff --git a/examples/linkaudio/AudioPlatform_Jack.cpp b/examples/linkaudio/AudioPlatform_Jack.cpp index 87b7b36..76a778d 100644 --- a/examples/linkaudio/AudioPlatform_Jack.cpp +++ b/examples/linkaudio/AudioPlatform_Jack.cpp @@ -30,8 +30,8 @@ namespace linkaudio AudioPlatform::AudioPlatform(Link& link) : mEngine(link) , mSampleTime(0.) - , mpJackClient(NULL) - , mpJackPorts(NULL) + , mpJackClient(nullptr) + , mpJackPorts(nullptr) { initialize(); start(); @@ -49,6 +49,20 @@ int AudioPlatform::audioCallback(jack_nframes_t nframes, void* pvUserData) return pAudioPlatform->audioCallback(nframes); } +void AudioPlatform::latencyCallback(jack_latency_callback_mode_t, void* pvUserData) +{ + AudioPlatform* pAudioPlatform = static_cast<AudioPlatform*>(pvUserData); + pAudioPlatform->updateLatency(); +} + +void AudioPlatform::updateLatency() +{ + jack_latency_range_t latencyRange; + jack_port_get_latency_range(mpJackPorts[0], JackPlaybackLatency, &latencyRange); + mEngine.mOutputLatency.store( + std::chrono::microseconds(llround(1.0e6 * latencyRange.max / mEngine.mSampleRate))); +} + int AudioPlatform::audioCallback(jack_nframes_t nframes) { using namespace std::chrono; @@ -58,7 +72,7 @@ int AudioPlatform::audioCallback(jack_nframes_t nframes) mSampleTime += nframes; - const auto bufferBeginAtOutput = hostTime + engine.mOutputLatency; + const auto bufferBeginAtOutput = hostTime + engine.mOutputLatency.load(); engine.audioCallback(bufferBeginAtOutput, nframes); @@ -76,7 +90,7 @@ void AudioPlatform::initialize() { jack_status_t status = JackFailure; mpJackClient = jack_client_open("LinkHut", JackNullOption, &status); - if (mpJackClient == NULL) + if (mpJackClient == nullptr) { std::cerr << "Could not initialize Audio Engine. "; std::cerr << "JACK: " << std::endl; @@ -104,15 +118,23 @@ void AudioPlatform::initialize() std::cerr << "Client protocol version mismatch." << std::endl; std::cerr << std::endl; std::terminate(); - }; + } + + const double bufferSize = jack_get_buffer_size(mpJackClient); + const double sampleRate = jack_get_sample_rate(mpJackClient); + mEngine.setBufferSize(static_cast<std::size_t>(bufferSize)); + mEngine.setSampleRate(sampleRate); + + jack_set_latency_callback(mpJackClient, AudioPlatform::latencyCallback, this); mpJackPorts = new jack_port_t*[2]; + for (int k = 0; k < 2; ++k) { const std::string port_name = "out_" + std::to_string(k + 1); mpJackPorts[k] = jack_port_register( mpJackClient, port_name.c_str(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - if (mpJackPorts[k] == NULL) + if (mpJackPorts[k] == nullptr) { std::cerr << "Could not get Audio Device. " << std::endl; jack_client_close(mpJackClient); @@ -121,15 +143,6 @@ void AudioPlatform::initialize() } jack_set_process_callback(mpJackClient, AudioPlatform::audioCallback, this); - - const double bufferSize = jack_get_buffer_size(mpJackClient); - const double sampleRate = jack_get_sample_rate(mpJackClient); - - mEngine.setBufferSize(static_cast<std::size_t>(bufferSize)); - mEngine.setSampleRate(sampleRate); - - mEngine.mOutputLatency = - std::chrono::microseconds(llround(1.0e6 * bufferSize / sampleRate)); } void AudioPlatform::uninitialize() @@ -137,13 +150,13 @@ void AudioPlatform::uninitialize() for (int k = 0; k < 2; ++k) { jack_port_unregister(mpJackClient, mpJackPorts[k]); - mpJackPorts[k] = NULL; + mpJackPorts[k] = nullptr; } delete[] mpJackPorts; - mpJackPorts = NULL; + mpJackPorts = nullptr; jack_client_close(mpJackClient); - mpJackClient = NULL; + mpJackClient = nullptr; } void AudioPlatform::start() @@ -151,7 +164,7 @@ void AudioPlatform::start() jack_activate(mpJackClient); const char** playback_ports = jack_get_ports( - mpJackClient, 0, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput | JackPortIsPhysical); + mpJackClient, nullptr, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput | JackPortIsPhysical); if (playback_ports) { diff --git a/examples/linkaudio/AudioPlatform_Jack.hpp b/examples/linkaudio/AudioPlatform_Jack.hpp index 5233e4a..0b7999c 100644 --- a/examples/linkaudio/AudioPlatform_Jack.hpp +++ b/examples/linkaudio/AudioPlatform_Jack.hpp @@ -40,6 +40,8 @@ public: private: static int audioCallback(jack_nframes_t nframes, void* pvUserData); int audioCallback(jack_nframes_t nframes); + static void latencyCallback(jack_latency_callback_mode_t mode, void* pvUserData); + void updateLatency(); void initialize(); void uninitialize(); diff --git a/examples/linkaudio/AudioPlatform_Portaudio.cpp b/examples/linkaudio/AudioPlatform_Portaudio.cpp index d6b4e3f..17e7e05 100644 --- a/examples/linkaudio/AudioPlatform_Portaudio.cpp +++ b/examples/linkaudio/AudioPlatform_Portaudio.cpp @@ -59,7 +59,7 @@ int AudioPlatform::audioCallback(const void* /*inputBuffer*/, platform.mSampleTime += inNumFrames; - const auto bufferBeginAtOutput = hostTime + engine.mOutputLatency; + const auto bufferBeginAtOutput = hostTime + engine.mOutputLatency.load(); engine.audioCallback(bufferBeginAtOutput, inNumFrames); @@ -94,8 +94,8 @@ void AudioPlatform::initialize() outputParameters.suggestedLatency = Pa_GetDeviceInfo(outputParameters.device)->defaultLowOutputLatency; outputParameters.hostApiSpecificStreamInfo = nullptr; - mEngine.mOutputLatency = - std::chrono::microseconds(llround(outputParameters.suggestedLatency * 1.0e6)); + mEngine.mOutputLatency.store( + std::chrono::microseconds(llround(outputParameters.suggestedLatency * 1.0e6))); result = Pa_OpenStream(&pStream, nullptr, &outputParameters, mEngine.mSampleRate, mEngine.mBuffer.size(), paClipOff, &audioCallback, this); diff --git a/examples/linkaudio/AudioPlatform_Wasapi.cpp b/examples/linkaudio/AudioPlatform_Wasapi.cpp index 4bdd912..efb73f8 100644 --- a/examples/linkaudio/AudioPlatform_Wasapi.cpp +++ b/examples/linkaudio/AudioPlatform_Wasapi.cpp @@ -67,6 +67,7 @@ DWORD renderAudioRunloop(LPVOID lpParam) AudioPlatform::AudioPlatform(Link& link) : mEngine(link) + , mSampleTime(0) , mDevice(nullptr) , mAudioClient(nullptr) , mRenderClient(nullptr) @@ -74,7 +75,6 @@ AudioPlatform::AudioPlatform(Link& link) , mEventHandle(nullptr) , mAudioThreadHandle(nullptr) , mIsRunning(false) - , mSampleTime(0) { initialize(); mEngine.setBufferSize(bufferSize()); @@ -278,7 +278,7 @@ DWORD AudioPlatform::audioRunloop() mSampleTime += numSamples; - const auto bufferBeginAtOutput = hostTime + mEngine.mOutputLatency; + const auto bufferBeginAtOutput = hostTime + mEngine.mOutputLatency.load(); mEngine.audioCallback(bufferBeginAtOutput, numSamples); diff --git a/examples/linkhut/main.cpp b/examples/linkhut/main.cpp index 6f17f2d..7bd0fcf 100644 --- a/examples/linkhut/main.cpp +++ b/examples/linkhut/main.cpp @@ -22,6 +22,7 @@ #include <algorithm> #include <atomic> #include <chrono> +#include <iomanip> #include <iostream> #include <thread> #if defined(LINK_PLATFORM_UNIX) @@ -42,7 +43,6 @@ struct State , link(120.) , audioPlatform(link) { - link.enable(true); } }; @@ -76,6 +76,7 @@ void printHelp() { std::cout << std::endl << " < L I N K H U T >" << std::endl << std::endl; std::cout << "usage:" << std::endl; + std::cout << " enable / disable Link: a" << std::endl; std::cout << " start / stop: space" << std::endl; std::cout << " decrease / increase tempo: w / e" << std::endl; std::cout << " decrease / increase quantum: r / t" << std::endl; @@ -83,20 +84,30 @@ void printHelp() std::cout << " quit: q" << std::endl << std::endl; } +void printStateHeader() +{ + std::cout + << "enabled | num peers | quantum | start stop sync | tempo | beats | metro" + << std::endl; +} + void printState(const std::chrono::microseconds time, const ableton::Link::SessionState sessionState, + const bool linkEnabled, const std::size_t numPeers, const double quantum, const bool startStopSyncOn) { + using namespace std; + const auto enabled = linkEnabled ? "yes" : "no"; const auto beats = sessionState.beatAtTime(time, quantum); const auto phase = sessionState.phaseAtTime(time, quantum); - const auto startStop = startStopSyncOn ? "on" : "off"; - std::cout << std::defaultfloat << "peers: " << numPeers << " | " - << "quantum: " << quantum << " | " - << "start stop sync: " << startStop << " | " - << "tempo: " << sessionState.tempo() << " | " << std::fixed - << "beats: " << beats << " | "; + const auto startStop = startStopSyncOn ? "yes" : "no"; + const auto isPlaying = sessionState.isPlaying() ? "[playing]" : "[stopped]"; + cout << defaultfloat << left << setw(7) << enabled << " | " << setw(9) << numPeers + << " | " << setw(7) << quantum << " | " << setw(3) << startStop << " " << setw(11) + << isPlaying << " | " << fixed << setw(7) << sessionState.tempo() << " | " << fixed + << setprecision(2) << setw(7) << beats << " | "; for (int i = 0; i < ceil(quantum); ++i) { if (i < phase) @@ -137,6 +148,9 @@ void input(State& state) state.running = false; clearLine(); return; + case 'a': + state.link.enable(!state.link.isEnabled()); + break; case 'w': engine.setTempo(tempo - 1); break; @@ -173,6 +187,7 @@ int main(int, char**) { State state; printHelp(); + printStateHeader(); std::thread thread(input, std::ref(state)); disableBufferedInput(); @@ -180,7 +195,7 @@ int main(int, char**) { const auto time = state.link.clock().micros(); auto sessionState = state.link.captureAppSessionState(); - printState(time, sessionState, state.link.numPeers(), + printState(time, sessionState, state.link.isEnabled(), state.link.numPeers(), state.audioPlatform.mEngine.quantum(), state.audioPlatform.mEngine.isStartStopSyncEnabled()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); diff --git a/examples/qlinkhut/Controller.cpp b/examples/qlinkhut/Controller.cpp deleted file mode 100644 index ce9dc30..0000000 --- a/examples/qlinkhut/Controller.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - * - * If you would like to incorporate Link into a proprietary software application, - * please contact <link-devs@ableton.com>. - */ - -#include "Controller.hpp" - -namespace ableton -{ -namespace qlinkhut -{ - -Controller::Controller() - : mTempo(120) - , mLink(mTempo) - , mAudioPlatform(mLink) -{ - mLink.setNumPeersCallback([this](std::size_t) { Q_EMIT onNumberOfPeersChanged(); }); - mLink.setTempoCallback([this](const double bpm) { - mTempo = bpm; - Q_EMIT onTempoChanged(); - }); -} - -void Controller::setIsPlaying(bool isPlaying) -{ - if (isPlaying) - { - mAudioPlatform.mEngine.startPlaying(); - } - else - { - mAudioPlatform.mEngine.stopPlaying(); - } -} - -bool Controller::isPlaying() -{ - return mAudioPlatform.mEngine.isPlaying(); -} - -void Controller::setTempo(double bpm) -{ - mAudioPlatform.mEngine.setTempo(bpm); -} - -double Controller::tempo() -{ - return mTempo; -} - -double Controller::quantum() -{ - return mAudioPlatform.mEngine.quantum(); -} - -void Controller::setQuantum(const double quantum) -{ - mAudioPlatform.mEngine.setQuantum(quantum); - Q_EMIT onQuantumChanged(); -} - -unsigned long Controller::numberOfPeers() -{ - return static_cast<unsigned long>(mLink.numPeers()); -} - -void Controller::setLinkEnabled(const bool isEnabled) -{ - mLink.enable(isEnabled); - Q_EMIT onIsLinkEnabledChanged(); -} - -bool Controller::isLinkEnabled() -{ - return mLink.isEnabled(); -} - -void Controller::setStartStopSyncEnabled(const bool isEnabled) -{ - mAudioPlatform.mEngine.setStartStopSyncEnabled(isEnabled); - Q_EMIT onIsStartStopSyncEnabledChanged(); -} - -bool Controller::isStartStopSyncEnabled() -{ - return mAudioPlatform.mEngine.isStartStopSyncEnabled(); -} - -double Controller::beatTime() -{ - return mAudioPlatform.mEngine.beatTime(); -} - -} // namespace qlinkhut -} // namespace ableton diff --git a/examples/qlinkhut/Controller.hpp b/examples/qlinkhut/Controller.hpp deleted file mode 100644 index d66ebca..0000000 --- a/examples/qlinkhut/Controller.hpp +++ /dev/null @@ -1,78 +0,0 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - * - * If you would like to incorporate Link into a proprietary software application, - * please contact <link-devs@ableton.com>. - */ - -#pragma once - -#include "AudioPlatform.hpp" -#include <QtCore/QObject> - -namespace ableton -{ -namespace qlinkhut -{ - -class Controller : public QObject -{ - Q_OBJECT - Q_DISABLE_COPY(Controller) - -public: - Controller(); - - void setIsPlaying(bool); - bool isPlaying(); - Q_SIGNAL void onIsPlayingChanged(); - Q_PROPERTY(bool isPlaying READ isPlaying WRITE setIsPlaying) - - void setTempo(double); - double tempo(); - Q_SIGNAL void onTempoChanged(); - Q_PROPERTY(double tempo READ tempo WRITE setTempo NOTIFY onTempoChanged) - - void setQuantum(double quantum); - double quantum(); - Q_SIGNAL void onQuantumChanged(); - Q_PROPERTY(double quantum READ quantum WRITE setQuantum NOTIFY onQuantumChanged) - - unsigned long numberOfPeers(); - Q_SIGNAL void onNumberOfPeersChanged(); - Q_PROPERTY(unsigned long numberOfPeers READ numberOfPeers NOTIFY onNumberOfPeersChanged) - - void setLinkEnabled(bool isEnabled); - bool isLinkEnabled(); - Q_SIGNAL void onIsLinkEnabledChanged(); - Q_PROPERTY(bool isLinkEnabled WRITE setLinkEnabled READ isLinkEnabled NOTIFY - onIsLinkEnabledChanged) - - void setStartStopSyncEnabled(bool isEnabled); - bool isStartStopSyncEnabled(); - Q_SIGNAL void onIsStartStopSyncEnabledChanged(); - Q_PROPERTY(bool isStartStopSyncEnabled WRITE setStartStopSyncEnabled READ - isStartStopSyncEnabled NOTIFY onIsStartStopSyncEnabledChanged) - - Q_INVOKABLE double beatTime(); - -private: - double mTempo; - Link mLink; - linkaudio::AudioPlatform mAudioPlatform; -}; - -} // namespace qlinkhut -} // namespace ableton diff --git a/examples/qlinkhut/main.qml b/examples/qlinkhut/main.qml deleted file mode 100644 index e31e837..0000000 --- a/examples/qlinkhut/main.qml +++ /dev/null @@ -1,351 +0,0 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - * - * If you would like to incorporate Link into a proprietary software application, - * please contact <link-devs@ableton.com>. - */ - -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Layouts 1.1 - -Item { - width: 768 - height: 576 - - Rectangle { - id: linkView - x: 31 - y: 25 - width: 170 - height: 50 - border.color: "#404040" - border.width: 1 - color: controller.isLinkEnabled ? "#E6E6E6" : "#FFFFFF" - - Label { - id: linkLabel - property var linkEnabledText: "0 Links" - text: controller.isLinkEnabled ? linkEnabledText : "Link" - color: "#404040" - font.pixelSize: 36 - font.family: "Calibri" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - Connections { - target: controller - - onNumberOfPeersChanged: { - linkLabel.linkEnabledText = controller.numberOfPeers + " Link" - linkLabel.linkEnabledText = controller.numberOfPeers == 1 - ? linkLabel.linkEnabledText - : linkLabel.linkEnabledText + "s" - } - } - } - - MouseArea { - anchors.fill: parent - - onPressed: { - controller.isLinkEnabled = !controller.isLinkEnabled - } - } - } - - Rectangle { - id: startStopSyncView - x: parent.width - width - 31 - y: 25 - width: 300 - height: 50 - - border.color: "#404040" - border.width: 1 - color: controller.isStartStopSyncEnabled ? "#E6E6E6" : "#FFFFFF" - - Label { - id: startStopSyncLabel - text: controller.isStartStopSyncEnabled ? - "StartStopSync On" : "StartStopSync Off" - color: "#404040" - font.pixelSize: 36 - font.family: "Calibri" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - anchors.fill: parent - - onPressed: { - controller.isStartStopSyncEnabled = !controller.isStartStopSyncEnabled - } - } - } - - Rectangle { - id: loopView - x: 30 - y: 90 - width: 708 - height: 328 - - function onQuantumChanged() { - for(var i = loopView.children.length; i > 0 ; i--) { - loopView.children[i-1].destroy() - } - - var numSegments = Math.ceil(controller.quantum) - for (var i = 0; i < numSegments; i++) { - var width = loopView.width / controller.quantum - var x = i * width - var component = Qt.createComponent("BeatTile.qml"); - var tile = component.createObject(loopView, { - "index": i, - "x": x, - "width": x + width > loopView.width ? loopView.width - x : width, - }) - tile.activeColor = i == 0 ? "#FF6A00" : "#FFD500" - } - } - - Connections { - target: controller - - onQuantumChanged: { - loopView.onQuantumChanged() - } - } - - Timer { - interval: 8 - running: true - repeat: true - property var last: 1 - onTriggered: { - var index = controller.isPlaying - ? Math.floor(controller.beatTime() % controller.quantum) - : controller.quantum - var countIn = index < 0; - var beat = countIn ? controller.quantum + index : index - for(var i = 0; i < loopView.children.length ; i++) { - loopView.children[i].countIn = countIn - loopView.children[i].currentBeat = beat - } - } - } - - Component.onCompleted: { - onQuantumChanged() - } - } - - Label { - x: 31 - y: 460 - width: 170 - height: 30 - text: "Quantum"; - color: "#404040" - font.pixelSize: 24 - font.family: "Calibri" - horizontalAlignment: Text.AlignHCenter - } - - Rectangle { - id: quantumView - x: 31 - y: 500 - width: 170 - height: 50 - border.width : 1 - border.color: "#404040" - - Label { - id: quantumLabel - text: controller.quantum.toFixed() - color: "#404040" - font.pixelSize: 36 - font.family: "Calibri" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - anchors.fill: parent - - property var clickQuantum: 0 - property var clickY: 0 - - onPressed: { - clickQuantum = controller.quantum - clickY = mouseY - } - - onPositionChanged: { - var quantum = clickQuantum + 0.1 * (clickY - mouseY) - controller.quantum = Math.max(1, quantum.toFixed()) - } - - onDoubleClicked: { - controller.quantum = 4 - } - } - } - - Label { - x: (parent.width - width) / 2 - 90 - y: 460 - width: 170 - height: 30 - text: "Tempo"; - color: "#404040" - font.pixelSize: 24 - font.family: "Calibri" - horizontalAlignment: Text.AlignHCenter - } - - - Rectangle { - id: tempoView - x: (parent.width - width) / 2 - 90 - y: 500 - width: 170 - height: 50 - border.width : 1 - border.color: "#404040" - - Label { - id: tempoLabel - text: controller.tempo.toFixed(2) - color: "#404040" - font.pixelSize: 36 - font.family: "Calibri" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - anchors.fill: parent - - property var clickTempo: 0 - property var clickY: 0 - - onPressed: { - clickTempo = controller.tempo - clickY = mouseY - } - - onPositionChanged: { - var tempo = clickTempo + 0.5 * (clickY - mouseY) - controller.tempo = tempo - } - - onDoubleClicked: { - controller.tempo = 120 - } - } - } - - Label { - id: beatTimeView - x: (parent.width - width) / 2 + 90 - y: 460 - width: 170 - height: 30 - text: "Beats" - color: "#404040" - font.pixelSize: 24 - font.family: "Calibri" - horizontalAlignment: Text.AlignHCenter - } - - Rectangle { - id: beatTimeIntView - x: (parent.width - width) / 2 + 90 - y: 500 - width: 170 - height: 50 - border.width: 1 - border.color: "#404040" - - Label { - id: beatTimeText - color: "#404040" - font.pixelSize: 36 - font.family: "Calibri" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } - - Timer { - id: timer - interval: 10 - repeat: true - running: true - - onTriggered: { - var beatTime = controller.beatTime() - beatTimeText.text = beatTime.toFixed(1) - transportText.text = controller.isPlaying ? "Pause" : "Play"; - transportView.color = controller.isPlaying ? "#E6E6E6" : "#FFFFFF"; - } - } - - Rectangle { - id: transportView - x: parent.width - width - 31 - y: 500 - width: 170 - height: 50 - border.width: 1 - border.color: "#404040" - - Label { - id: transportText - color: "#404040" - font.pixelSize: 36 - font.family: "Calibri" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - anchors.fill: parent - - onPressed: { - controller.isPlaying = !controller.isPlaying - } - } - } - - Item { - focus: true - Keys.onPressed: { - if ( event.key == 32) { - controller.isPlaying = !controller.isPlaying - } - } - } -} diff --git a/examples/qlinkhut/resources.qrc b/examples/qlinkhut/resources.qrc deleted file mode 100644 index 74f2d2f..0000000 --- a/examples/qlinkhut/resources.qrc +++ /dev/null @@ -1,6 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file>BeatTile.qml</file> - <file>main.qml</file> - </qresource> -</RCC> diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 987cec0..0d9c71e 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -57,7 +57,6 @@ set(link_discovery_HEADERS ${link_discovery_DIR}/PeerGateway.hpp ${link_discovery_DIR}/PeerGateways.hpp ${link_discovery_DIR}/Service.hpp - ${link_discovery_DIR}/Socket.hpp ${link_discovery_DIR}/UdpMessenger.hpp ${link_discovery_DIR}/v1/Messages.hpp PARENT_SCOPE @@ -73,7 +72,6 @@ set(link_discovery_HEADERS set(link_platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ableton/platforms) set(link_platform_HEADERS ${link_platform_DIR}/Config.hpp - ${link_platform_DIR}/asio/AsioService.hpp ${link_platform_DIR}/asio/AsioTimer.hpp ${link_platform_DIR}/asio/AsioWrapper.hpp ${link_platform_DIR}/asio/Context.hpp @@ -82,7 +80,16 @@ set(link_platform_HEADERS ${link_platform_DIR}/asio/Util.hpp ) -if(UNIX) +if(ESP_PLATFORM) + set(link_platform_HEADERS + ${link_platform_HEADERS} + ${link_platform_DIR}/esp32/Clock.hpp + ${link_platform_DIR}/esp32/Context.hpp + ${link_platform_DIR}/esp32/Esp32.hpp + ${link_platform_DIR}/esp32/Random.hpp + ${link_platform_DIR}/esp32/ScanIpIfAddrs.hpp + ) +elseif(UNIX) set(link_platform_HEADERS ${link_platform_HEADERS} ${link_platform_DIR}/posix/ScanIpIfAddrs.hpp @@ -93,6 +100,7 @@ if(UNIX) ${link_platform_HEADERS} ${link_platform_DIR}/darwin/Clock.hpp ${link_platform_DIR}/darwin/Darwin.hpp + ${link_platform_DIR}/stl/Random.hpp ) elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set(link_platform_HEADERS @@ -100,13 +108,16 @@ if(UNIX) ${link_platform_DIR}/linux/Clock.hpp ${link_platform_DIR}/linux/Linux.hpp ${link_platform_DIR}/stl/Clock.hpp + ${link_platform_DIR}/stl/Random.hpp ) endif() elseif(WIN32) set(link_platform_HEADERS ${link_platform_HEADERS} + ${link_platform_DIR}/stl/Random.hpp ${link_platform_DIR}/windows/Clock.hpp ${link_platform_DIR}/windows/ScanIpIfAddrs.hpp + ${link_platform_DIR}/windows/Windows.hpp ) endif() set(link_platform_HEADERS diff --git a/include/ableton/Link.hpp b/include/ableton/Link.hpp index a5b76c5..9ba8b3a 100644 --- a/include/ableton/Link.hpp +++ b/include/ableton/Link.hpp @@ -29,8 +29,12 @@ namespace ableton { -/*! @class Link - * @brief Class that represents a participant in a Link session. +/*! @class Link and BasicLink + * @brief Classes representing a participant in a Link session. + * The BasicLink type allows to customize the clock. The Link type + * uses the recommended platform-dependent representation of the + * system clock as defined in platforms/Config.hpp. + * It's preferred to use Link instead of BasicLink. * * @discussion Each Link instance has its own session state which * represents a beat timeline and a transport start/stop state. The @@ -64,21 +68,26 @@ namespace ableton * state from both the audio thread and an application thread * concurrently is not advised and will potentially lead to unexpected * behavior. + * + * Only use the BasicLink class if the default platform clock does not + * fulfill other requirements of the client application. Please note this + * will require providing a custom Clock implementation. See the clock() + * documentation for details. */ -class Link +template <typename Clock> +class BasicLink { public: - using Clock = link::platform::Clock; class SessionState; /*! @brief Construct with an initial tempo. */ - Link(double bpm); + BasicLink(double bpm); /*! @brief Link instances cannot be copied or moved */ - Link(const Link&) = delete; - Link& operator=(const Link&) = delete; - Link(Link&&) = delete; - Link& operator=(Link&&) = delete; + BasicLink(const BasicLink<Clock>&) = delete; + BasicLink& operator=(const BasicLink<Clock>&) = delete; + BasicLink(BasicLink<Clock>&&) = delete; + BasicLink& operator=(BasicLink<Clock>&&) = delete; /*! @brief Is Link currently enabled? * Thread-safe: yes @@ -152,13 +161,10 @@ public: * Thread-safe: yes * Realtime-safe: yes * - * @discussion The Clock type is a platform-dependent - * representation of the system clock. It exposes a ticks() method - * that returns the current ticks of the system clock as well as - * micros(), which is a normalized representation of the current system - * time in std::chrono::microseconds. It also provides conversion - * functions ticksToMicros() and microsToTicks() to faciliate - * converting between these units. + * @discussion The Clock type is a platform-dependent representation + * of the system clock. It exposes a micros() method, which is a + * normalized representation of the current system time in + * std::chrono::microseconds. */ Clock clock() const; @@ -238,7 +244,12 @@ public: public: SessionState(const link::ApiState state, const bool bRespectQuantum); - /*! @brief: The tempo of the timeline, in bpm */ + /*! @brief: The tempo of the timeline, in Beats Per Minute. + * + * @discussion This is a stable value that is appropriate for display + * to the user. Beat time progress will not necessarily match this tempo + * exactly because of clock drift compensation. + */ double tempo() const; /*! @brief: Set the timeline tempo to the given bpm value, taking @@ -354,19 +365,38 @@ public: bool isPlaying, std::chrono::microseconds time, double beat, double quantum); private: - friend Link; + friend BasicLink<Clock>; link::ApiState mOriginalState; link::ApiState mState; bool mbRespectQuantum; }; private: + using Controller = ableton::link::Controller< + link::PeerCountCallback, + link::TempoCallback, + link::StartStopStateCallback, + Clock, + link::platform::Random, + link::platform::IoContext>; + std::mutex mCallbackMutex; - link::PeerCountCallback mPeerCountCallback; - link::TempoCallback mTempoCallback; - link::StartStopStateCallback mStartStopCallback; + link::PeerCountCallback mPeerCountCallback = [](std::size_t) {}; + link::TempoCallback mTempoCallback = [](link::Tempo) {}; + link::StartStopStateCallback mStartStopCallback = [](bool) {}; Clock mClock; - link::platform::Controller mController; + Controller mController; +}; + +class Link : public BasicLink<link::platform::Clock> +{ +public: + using Clock = link::platform::Clock; + + Link(double bpm) + : BasicLink(bpm) + { + } }; } // namespace ableton diff --git a/include/ableton/Link.ipp b/include/ableton/Link.ipp index 381bcd7..908f8e8 100644 --- a/include/ableton/Link.ipp +++ b/include/ableton/Link.ipp @@ -26,13 +26,12 @@ namespace ableton namespace detail { -inline Link::SessionState toSessionState( +template <typename Clock> +inline typename BasicLink<Clock>::SessionState toSessionState( const link::ClientState& state, const bool isConnected) { - const auto time = state.timeline.fromBeats(state.startStopState.beats); - const auto startStopState = - link::ApiStartStopState{state.startStopState.isPlaying, time}; - return {{state.timeline, startStopState}, isConnected}; + return {{state.timeline, {state.startStopState.isPlaying, state.startStopState.time}}, + isConnected}; } inline link::IncomingClientState toIncomingClientState(const link::ApiState& state, @@ -44,20 +43,18 @@ inline link::IncomingClientState toIncomingClientState(const link::ApiState& sta : link::OptionalTimeline{}; const auto startStopState = originalState.startStopState != state.startStopState - ? link::OptionalStartStopState{{state.startStopState.isPlaying, - state.timeline.toBeats(state.startStopState.time), timestamp}} - : link::OptionalStartStopState{}; + ? link::OptionalClientStartStopState{{state.startStopState.isPlaying, + state.startStopState.time, timestamp}} + : link::OptionalClientStartStopState{}; return {timeline, startStopState, timestamp}; } } // namespace detail -inline Link::Link(const double bpm) - : mPeerCountCallback([](std::size_t) {}) - , mTempoCallback([](link::Tempo) {}) - , mStartStopCallback([](bool) {}) - , mClock{} - , mController(link::Tempo(bpm), +template <typename Clock> +inline BasicLink<Clock>::BasicLink(const double bpm) + : mController( + link::Tempo(bpm), [this](const std::size_t peers) { std::lock_guard<std::mutex> lock(mCallbackMutex); mPeerCountCallback(peers); @@ -75,74 +72,87 @@ inline Link::Link(const double bpm) { } -inline bool Link::isEnabled() const +template <typename Clock> +inline bool BasicLink<Clock>::isEnabled() const { return mController.isEnabled(); } -inline void Link::enable(const bool bEnable) +template <typename Clock> +inline void BasicLink<Clock>::enable(const bool bEnable) { mController.enable(bEnable); } -inline bool Link::isStartStopSyncEnabled() const +template <typename Clock> +inline bool BasicLink<Clock>::isStartStopSyncEnabled() const { return mController.isStartStopSyncEnabled(); } -inline void Link::enableStartStopSync(bool bEnable) +template <typename Clock> +inline void BasicLink<Clock>::enableStartStopSync(bool bEnable) { mController.enableStartStopSync(bEnable); } -inline std::size_t Link::numPeers() const +template <typename Clock> +inline std::size_t BasicLink<Clock>::numPeers() const { return mController.numPeers(); } +template <typename Clock> template <typename Callback> -void Link::setNumPeersCallback(Callback callback) +void BasicLink<Clock>::setNumPeersCallback(Callback callback) { std::lock_guard<std::mutex> lock(mCallbackMutex); mPeerCountCallback = [callback](const std::size_t numPeers) { callback(numPeers); }; } +template <typename Clock> template <typename Callback> -void Link::setTempoCallback(Callback callback) +void BasicLink<Clock>::setTempoCallback(Callback callback) { std::lock_guard<std::mutex> lock(mCallbackMutex); mTempoCallback = [callback](const link::Tempo tempo) { callback(tempo.bpm()); }; } +template <typename Clock> template <typename Callback> -void Link::setStartStopCallback(Callback callback) +void BasicLink<Clock>::setStartStopCallback(Callback callback) { std::lock_guard<std::mutex> lock(mCallbackMutex); mStartStopCallback = callback; } -inline Link::Clock Link::clock() const +template <typename Clock> +inline Clock BasicLink<Clock>::clock() const { return mClock; } -inline Link::SessionState Link::captureAudioSessionState() const +template <typename Clock> +inline typename BasicLink<Clock>::SessionState BasicLink<Clock>::captureAudioSessionState() const { - return detail::toSessionState(mController.clientStateRtSafe(), numPeers() > 0); + return detail::toSessionState<Clock>(mController.clientStateRtSafe(), numPeers() > 0); } -inline void Link::commitAudioSessionState(const Link::SessionState state) +template <typename Clock> +inline void BasicLink<Clock>::commitAudioSessionState(const typename BasicLink<Clock>::SessionState state) { mController.setClientStateRtSafe( detail::toIncomingClientState(state.mState, state.mOriginalState, mClock.micros())); } -inline Link::SessionState Link::captureAppSessionState() const +template <typename Clock> +inline typename BasicLink<Clock>::SessionState BasicLink<Clock>::captureAppSessionState() const { - return detail::toSessionState(mController.clientState(), numPeers() > 0); + return detail::toSessionState<Clock>(mController.clientState(), numPeers() > 0); } -inline void Link::commitAppSessionState(const Link::SessionState state) +template <typename Clock> +inline void BasicLink<Clock>::commitAppSessionState(const typename BasicLink<Clock>::SessionState state) { mController.setClientState( detail::toIncomingClientState(state.mState, state.mOriginalState, mClock.micros())); @@ -150,7 +160,8 @@ inline void Link::commitAppSessionState(const Link::SessionState state) // Link::SessionState -inline Link::SessionState::SessionState( +template <typename Clock> +inline BasicLink<Clock>::SessionState::SessionState( const link::ApiState state, const bool bRespectQuantum) : mOriginalState(state) , mState(state) @@ -158,12 +169,14 @@ inline Link::SessionState::SessionState( { } -inline double Link::SessionState::tempo() const +template <typename Clock> +inline double BasicLink<Clock>::SessionState::tempo() const { return mState.timeline.tempo.bpm(); } -inline void Link::SessionState::setTempo( +template <typename Clock> +inline void BasicLink<Clock>::SessionState::setTempo( const double bpm, const std::chrono::microseconds atTime) { const auto desiredTl = link::clampTempo( @@ -172,28 +185,32 @@ inline void Link::SessionState::setTempo( mState.timeline.timeOrigin = desiredTl.fromBeats(mState.timeline.beatOrigin); } -inline double Link::SessionState::beatAtTime( +template <typename Clock> +inline double BasicLink<Clock>::SessionState::beatAtTime( const std::chrono::microseconds time, const double quantum) const { return link::toPhaseEncodedBeats(mState.timeline, time, link::Beats{quantum}) .floating(); } -inline double Link::SessionState::phaseAtTime( +template <typename Clock> +inline double BasicLink<Clock>::SessionState::phaseAtTime( const std::chrono::microseconds time, const double quantum) const { return link::phase(link::Beats{beatAtTime(time, quantum)}, link::Beats{quantum}) .floating(); } -inline std::chrono::microseconds Link::SessionState::timeAtBeat( +template <typename Clock> +inline std::chrono::microseconds BasicLink<Clock>::SessionState::timeAtBeat( const double beat, const double quantum) const { return link::fromPhaseEncodedBeats( mState.timeline, link::Beats{beat}, link::Beats{quantum}); } -inline void Link::SessionState::requestBeatAtTime( +template <typename Clock> +inline void BasicLink<Clock>::SessionState::requestBeatAtTime( const double beat, std::chrono::microseconds time, const double quantum) { if (mbRespectQuantum) @@ -206,7 +223,8 @@ inline void Link::SessionState::requestBeatAtTime( forceBeatAtTime(beat, time, quantum); } -inline void Link::SessionState::forceBeatAtTime( +template <typename Clock> +inline void BasicLink<Clock>::SessionState::forceBeatAtTime( const double beat, const std::chrono::microseconds time, const double quantum) { // There are two components to the beat adjustment: a phase shift @@ -220,23 +238,27 @@ inline void Link::SessionState::forceBeatAtTime( mState.timeline.beatOrigin + (link::Beats{beat} - closestInPhase); } -inline void Link::SessionState::setIsPlaying( +template <typename Clock> +inline void BasicLink<Clock>::SessionState::setIsPlaying( const bool isPlaying, const std::chrono::microseconds time) { mState.startStopState = {isPlaying, time}; } -inline bool Link::SessionState::isPlaying() const +template <typename Clock> +inline bool BasicLink<Clock>::SessionState::isPlaying() const { return mState.startStopState.isPlaying; } -inline std::chrono::microseconds Link::SessionState::timeForIsPlaying() const +template <typename Clock> +inline std::chrono::microseconds BasicLink<Clock>::SessionState::timeForIsPlaying() const { return mState.startStopState.time; } -inline void Link::SessionState::requestBeatAtStartPlayingTime( +template <typename Clock> +inline void BasicLink<Clock>::SessionState::requestBeatAtStartPlayingTime( const double beat, const double quantum) { if (isPlaying()) @@ -245,7 +267,8 @@ inline void Link::SessionState::requestBeatAtStartPlayingTime( } } -inline void Link::SessionState::setIsPlayingAndRequestBeatAtTime( +template <typename Clock> +inline void BasicLink<Clock>::SessionState::setIsPlayingAndRequestBeatAtTime( bool isPlaying, std::chrono::microseconds time, double beat, double quantum) { mState.startStopState = {isPlaying, time}; diff --git a/include/ableton/discovery/InterfaceScanner.hpp b/include/ableton/discovery/InterfaceScanner.hpp index b694528..cb3adad 100644 --- a/include/ableton/discovery/InterfaceScanner.hpp +++ b/include/ableton/discovery/InterfaceScanner.hpp @@ -95,8 +95,7 @@ InterfaceScanner<Callback, IoContext> makeInterfaceScanner( util::Injected<Callback> callback, util::Injected<IoContext> io) { - using namespace std; - return {period, move(callback), move(io)}; + return {period, std::move(callback), std::move(io)}; } } // namespace discovery diff --git a/include/ableton/discovery/IpV4Interface.hpp b/include/ableton/discovery/IpV4Interface.hpp index ad02fe3..9967f51 100644 --- a/include/ableton/discovery/IpV4Interface.hpp +++ b/include/ableton/discovery/IpV4Interface.hpp @@ -19,7 +19,7 @@ #pragma once -#include <ableton/platforms/asio/AsioService.hpp> +#include <ableton/platforms/asio/AsioWrapper.hpp> #include <ableton/util/Injected.hpp> namespace ableton @@ -29,7 +29,7 @@ namespace discovery inline asio::ip::udp::endpoint multicastEndpoint() { - return {asio::ip::address::from_string("224.76.78.75"), 20808}; + return {asio::ip::address_v4::from_string("224.76.78.75"), 20808}; } // Type tags for dispatching between unicast and multicast packets diff --git a/include/ableton/discovery/NetworkByteStreamSerializable.hpp b/include/ableton/discovery/NetworkByteStreamSerializable.hpp index 0a65415..6de27cc 100644 --- a/include/ableton/discovery/NetworkByteStreamSerializable.hpp +++ b/include/ableton/discovery/NetworkByteStreamSerializable.hpp @@ -24,6 +24,10 @@ #include <ableton/platforms/darwin/Darwin.hpp> #elif defined(LINK_PLATFORM_LINUX) #include <ableton/platforms/linux/Linux.hpp> +#elif defined(LINK_PLATFORM_WINDOWS) +#include <ableton/platforms/windows/Windows.hpp> +#elif defined(ESP_PLATFORM) +#include <ableton/platforms/esp32/Esp32.hpp> #endif #include <chrono> @@ -281,7 +285,7 @@ struct Deserialize<std::chrono::microseconds> static std::pair<std::chrono::microseconds, It> fromNetworkByteStream(It begin, It end) { using namespace std; - auto result = Deserialize<int64_t>::fromNetworkByteStream(move(begin), move(end)); + auto result = Deserialize<int64_t>::fromNetworkByteStream(std::move(begin), std::move(end)); return make_pair(chrono::microseconds{result.first}, result.second); } }; @@ -357,8 +361,8 @@ struct Deserialize<std::array<T, Size>> using namespace std; array<T, Size> result{}; auto resultIt = - detail::deserializeContainer<T>(move(begin), move(end), move(result.begin()), Size); - return make_pair(move(result), move(resultIt)); + detail::deserializeContainer<T>(std::move(begin), std::move(end), std::move(result.begin()), Size); + return make_pair(std::move(result), std::move(resultIt)); } }; @@ -385,11 +389,11 @@ struct Deserialize<std::vector<T, Alloc>> { using namespace std; auto result_size = - Deserialize<uint32_t>::fromNetworkByteStream(move(bytesBegin), bytesEnd); + Deserialize<uint32_t>::fromNetworkByteStream(std::move(bytesBegin), bytesEnd); vector<T, Alloc> result; auto resultIt = detail::deserializeContainer<T>( - move(result_size.second), move(bytesEnd), back_inserter(result), result_size.first); - return make_pair(move(result), move(resultIt)); + std::move(result_size.second), std::move(bytesEnd), back_inserter(result), result_size.first); + return make_pair(std::move(result), std::move(resultIt)); } }; @@ -416,7 +420,7 @@ struct Deserialize<std::tuple<X, Y>> using namespace std; auto xres = Deserialize<X>::fromNetworkByteStream(begin, end); auto yres = Deserialize<Y>::fromNetworkByteStream(xres.second, end); - return make_pair(make_tuple(move(xres.first), move(yres.first)), move(yres.second)); + return make_pair(make_tuple(std::move(xres.first), std::move(yres.first)), std::move(yres.second)); } }; @@ -446,8 +450,8 @@ struct Deserialize<std::tuple<X, Y, Z>> auto xres = Deserialize<X>::fromNetworkByteStream(begin, end); auto yres = Deserialize<Y>::fromNetworkByteStream(xres.second, end); auto zres = Deserialize<Z>::fromNetworkByteStream(yres.second, end); - return make_pair(make_tuple(move(xres.first), move(yres.first), move(zres.first)), - move(zres.second)); + return make_pair(make_tuple(std::move(xres.first), std::move(yres.first), std::move(zres.first)), + std::move(zres.second)); } }; diff --git a/include/ableton/discovery/Payload.hpp b/include/ableton/discovery/Payload.hpp index d15b9f3..2c93241 100644 --- a/include/ableton/discovery/Payload.hpp +++ b/include/ableton/discovery/Payload.hpp @@ -21,6 +21,7 @@ #include <ableton/discovery/NetworkByteStreamSerializable.hpp> #include <functional> +#include <sstream> #include <unordered_map> namespace ableton @@ -56,7 +57,7 @@ struct PayloadEntryHeader Size size; tie(key, begin) = Deserialize<Key>::fromNetworkByteStream(begin, end); tie(size, begin) = Deserialize<Size>::fromNetworkByteStream(begin, end); - return make_pair(PayloadEntryHeader{move(key), move(size)}, move(begin)); + return make_pair(PayloadEntryHeader{std::move(key), std::move(size)}, std::move(begin)); } }; @@ -126,7 +127,7 @@ void parseByteStream(HandlerMap<It>& map, It bsBegin, const It bsEnd) auto handlerIt = map.find(header.key); if (handlerIt != end(map)) { - handlerIt->second(move(valueBegin), move(valueEnd)); + handlerIt->second(std::move(valueBegin), std::move(valueEnd)); } } } @@ -286,7 +287,7 @@ template <typename... Entries, typename It, typename... Handlers> void parsePayload(It begin, It end, Handlers... handlers) { using namespace std; - ParsePayload<Entries...>::parse(move(begin), move(end), move(handlers)...); + ParsePayload<Entries...>::parse(std::move(begin), std::move(end), std::move(handlers)...); } } // namespace discovery diff --git a/include/ableton/discovery/PeerGateway.hpp b/include/ableton/discovery/PeerGateway.hpp index 7091e82..2d35f03 100644 --- a/include/ableton/discovery/PeerGateway.hpp +++ b/include/ableton/discovery/PeerGateway.hpp @@ -21,7 +21,6 @@ #include <ableton/discovery/UdpMessenger.hpp> #include <ableton/discovery/v1/Messages.hpp> -#include <ableton/platforms/asio/AsioService.hpp> #include <ableton/util/SafeAsyncHandler.hpp> #include <memory> @@ -125,7 +124,7 @@ private: auto newTo = make_pair(mPruneTimer.now() + std::chrono::seconds(ttl), peerId); mPeerTimeouts.insert( upper_bound(begin(mPeerTimeouts), end(mPeerTimeouts), newTo, TimeoutCompare{}), - move(newTo)); + std::move(newTo)); sawPeer(*mObserver, nodeState); scheduleNextPruning(); @@ -244,8 +243,8 @@ IpV4Gateway<PeerObserver, NodeState, IoContext> makeIpV4Gateway( auto iface = makeIpV4Interface<v1::kMaxMessageSize>(injectRef(*io), addr); auto messenger = - makeUdpMessenger(injectVal(move(iface)), move(state), injectRef(*io), ttl, ttlRatio); - return {injectVal(move(messenger)), move(observer), move(io)}; + makeUdpMessenger(injectVal(std::move(iface)), std::move(state), injectRef(*io), ttl, ttlRatio); + return {injectVal(std::move(messenger)), std::move(observer), std::move(io)}; } } // namespace discovery diff --git a/include/ableton/discovery/PeerGateways.hpp b/include/ableton/discovery/PeerGateways.hpp index 16c2dac..ed5cfe5 100644 --- a/include/ableton/discovery/PeerGateways.hpp +++ b/include/ableton/discovery/PeerGateways.hpp @@ -222,7 +222,7 @@ std::unique_ptr<PeerGateways<NodeState, GatewayFactory, IoContext>> makePeerGate using namespace std; using Gateways = PeerGateways<NodeState, GatewayFactory, IoContext>; return unique_ptr<Gateways>{ - new Gateways{rescanPeriod, move(state), move(factory), move(io)}}; + new Gateways{rescanPeriod, std::move(state), std::move(factory), std::move(io)}}; } } // namespace discovery diff --git a/include/ableton/discovery/Socket.hpp b/include/ableton/discovery/Socket.hpp deleted file mode 100644 index cbc5993..0000000 --- a/include/ableton/discovery/Socket.hpp +++ /dev/null @@ -1,139 +0,0 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - * - * If you would like to incorporate Link into a proprietary software application, - * please contact <link-devs@ableton.com>. - */ - -#pragma once - -#include <ableton/platforms/asio/AsioService.hpp> -#include <ableton/platforms/asio/AsioWrapper.hpp> -#include <ableton/util/SafeAsyncHandler.hpp> -#include <array> -#include <cassert> - -namespace ableton -{ -namespace discovery -{ - -template <std::size_t MaxPacketSize> -struct Socket -{ - Socket(platforms::asio::AsioService& io) - : mpImpl(std::make_shared<Impl>(io)) - { - } - - Socket(const Socket&) = delete; - Socket& operator=(const Socket&) = delete; - - Socket(Socket&& rhs) - : mpImpl(std::move(rhs.mpImpl)) - { - } - - std::size_t send( - const uint8_t* const pData, const size_t numBytes, const asio::ip::udp::endpoint& to) - { - assert(numBytes < MaxPacketSize); - return mpImpl->mSocket.send_to(asio::buffer(pData, numBytes), to); - } - - template <typename Handler> - void receive(Handler handler) - { - mpImpl->mHandler = std::move(handler); - mpImpl->mSocket.async_receive_from( - asio::buffer(mpImpl->mReceiveBuffer, MaxPacketSize), mpImpl->mSenderEndpoint, - util::makeAsyncSafe(mpImpl)); - } - - asio::ip::udp::endpoint endpoint() const - { - return mpImpl->mSocket.local_endpoint(); - } - - struct Impl - { - Impl(platforms::asio::AsioService& io) - : mSocket(io.mService, asio::ip::udp::v4()) - { - } - - ~Impl() - { - // Ignore error codes in shutdown and close as the socket may - // have already been forcibly closed - asio::error_code ec; - mSocket.shutdown(asio::ip::udp::socket::shutdown_both, ec); - mSocket.close(ec); - } - - void operator()(const asio::error_code& error, const std::size_t numBytes) - { - if (!error && numBytes > 0 && numBytes <= MaxPacketSize) - { - const auto bufBegin = begin(mReceiveBuffer); - mHandler(mSenderEndpoint, bufBegin, bufBegin + static_cast<ptrdiff_t>(numBytes)); - } - } - - asio::ip::udp::socket mSocket; - asio::ip::udp::endpoint mSenderEndpoint; - using Buffer = std::array<uint8_t, MaxPacketSize>; - Buffer mReceiveBuffer; - using ByteIt = typename Buffer::const_iterator; - std::function<void(const asio::ip::udp::endpoint&, ByteIt, ByteIt)> mHandler; - }; - - std::shared_ptr<Impl> mpImpl; -}; - -// Configure an asio socket for receiving multicast messages -template <std::size_t MaxPacketSize> -void configureMulticastSocket(Socket<MaxPacketSize>& socket, - const asio::ip::address_v4& addr, - const asio::ip::udp::endpoint& multicastEndpoint) -{ - socket.mpImpl->mSocket.set_option(asio::ip::udp::socket::reuse_address(true)); - // ??? - socket.mpImpl->mSocket.set_option(asio::socket_base::broadcast(!addr.is_loopback())); - // ??? - socket.mpImpl->mSocket.set_option( - asio::ip::multicast::enable_loopback(addr.is_loopback())); - socket.mpImpl->mSocket.set_option(asio::ip::multicast::outbound_interface(addr)); - // Is from_string("0.0.0.0") best approach? - socket.mpImpl->mSocket.bind( - {asio::ip::address::from_string("0.0.0.0"), multicastEndpoint.port()}); - socket.mpImpl->mSocket.set_option( - asio::ip::multicast::join_group(multicastEndpoint.address().to_v4(), addr)); -} - -// Configure an asio socket for receiving unicast messages -template <std::size_t MaxPacketSize> -void configureUnicastSocket( - Socket<MaxPacketSize>& socket, const asio::ip::address_v4& addr) -{ - // ??? really necessary? - socket.mpImpl->mSocket.set_option( - asio::ip::multicast::enable_loopback(addr.is_loopback())); - socket.mpImpl->mSocket.set_option(asio::ip::multicast::outbound_interface(addr)); - socket.mpImpl->mSocket.bind(asio::ip::udp::endpoint{addr, 0}); -} - -} // namespace discovery -} // namespace ableton diff --git a/include/ableton/discovery/UdpMessenger.hpp b/include/ableton/discovery/UdpMessenger.hpp index d98f28a..c15c694 100644 --- a/include/ableton/discovery/UdpMessenger.hpp +++ b/include/ableton/discovery/UdpMessenger.hpp @@ -59,7 +59,7 @@ void sendUdpMessage(Interface& iface, v1::MessageBuffer buffer; const auto messageBegin = begin(buffer); const auto messageEnd = - v1::detail::encodeMessage(move(from), ttl, messageType, payload, messageBegin); + v1::detail::encodeMessage(std::move(from), ttl, messageType, payload, messageBegin); const auto numBytes = static_cast<size_t>(distance(messageBegin, messageEnd)); try { diff --git a/include/ableton/discovery/v1/Messages.hpp b/include/ableton/discovery/v1/Messages.hpp index 81d0955..73ba8c3 100644 --- a/include/ableton/discovery/v1/Messages.hpp +++ b/include/ableton/discovery/v1/Messages.hpp @@ -82,7 +82,7 @@ struct MessageHeader tie(header.ident, begin) = Deserialize<decltype(header.ident)>::fromNetworkByteStream(begin, end); - return make_pair(move(header), move(begin)); + return make_pair(std::move(header), std::move(begin)); } }; @@ -111,7 +111,7 @@ It encodeMessage(NodeId from, { return toNetworkByteStream( payload, toNetworkByteStream( - header, copy(begin(kProtocolHeader), end(kProtocolHeader), move(out)))); + header, copy(begin(kProtocolHeader), end(kProtocolHeader), std::move(out)))); } else { @@ -160,7 +160,7 @@ std::pair<MessageHeader<NodeId>, It> parseMessageHeader(It bytesBegin, const It tie(header, bytesBegin) = MessageHeader<NodeId>::fromNetworkByteStream( bytesBegin + protocolHeaderSize, bytesEnd); } - return make_pair(move(header), move(bytesBegin)); + return make_pair(std::move(header), std::move(bytesBegin)); } } // namespace v1 diff --git a/include/ableton/link/Beats.hpp b/include/ableton/link/Beats.hpp index 28e5482..bf3d45e 100644 --- a/include/ableton/link/Beats.hpp +++ b/include/ableton/link/Beats.hpp @@ -22,60 +22,79 @@ #include <ableton/discovery/NetworkByteStreamSerializable.hpp> #include <cmath> #include <cstdint> -#include <tuple> namespace ableton { namespace link { -struct Beats : std::tuple<std::int64_t> +struct Beats { Beats() = default; explicit Beats(const double beats) - : std::tuple<std::int64_t>(llround(beats * 1e6)) + : mValue(std::llround(beats * 1e6)) { } explicit Beats(const std::int64_t microBeats) - : std::tuple<std::int64_t>(microBeats) + : mValue(microBeats) { } double floating() const { - return microBeats() / 1e6; + return static_cast<double>(mValue) / 1e6; } std::int64_t microBeats() const { - return std::get<0>(*this); + return mValue; } Beats operator-() const { - return Beats{-microBeats()}; + return Beats{-mValue}; } friend Beats abs(const Beats b) { - return Beats{std::abs(b.microBeats())}; + return Beats{std::abs(b.mValue)}; } friend Beats operator+(const Beats lhs, const Beats rhs) { - return Beats{lhs.microBeats() + rhs.microBeats()}; + return Beats{lhs.mValue + rhs.mValue}; } friend Beats operator-(const Beats lhs, const Beats rhs) { - return Beats{lhs.microBeats() - rhs.microBeats()}; + return Beats{lhs.mValue - rhs.mValue}; } friend Beats operator%(const Beats lhs, const Beats rhs) { - return rhs == Beats{0.} ? Beats{0.} : Beats{lhs.microBeats() % rhs.microBeats()}; + return Beats{rhs.mValue == 0 ? 0 : (lhs.mValue % rhs.mValue)}; + } + + friend bool operator<(const Beats lhs, const Beats rhs) + { + return lhs.mValue < rhs.mValue; + } + + friend bool operator>(const Beats lhs, const Beats rhs) + { + return lhs.mValue > rhs.mValue; + } + + friend bool operator==(const Beats lhs, const Beats rhs) + { + return lhs.mValue == rhs.mValue; + } + + friend bool operator!=(const Beats lhs, const Beats rhs) + { + return lhs.mValue != rhs.mValue; } // Model the NetworkByteStreamSerializable concept @@ -97,6 +116,9 @@ struct Beats : std::tuple<std::int64_t> std::move(begin), std::move(end)); return std::make_pair(Beats{result.first}, std::move(result.second)); } + +private: + std::int64_t mValue = 0; }; } // namespace link diff --git a/include/ableton/link/CircularFifo.hpp b/include/ableton/link/CircularFifo.hpp index 5e3578f..8ee8a26 100644 --- a/include/ableton/link/CircularFifo.hpp +++ b/include/ableton/link/CircularFifo.hpp @@ -19,10 +19,12 @@ #pragma once -#include <ableton/link/Optional.hpp> #include <array> #include <atomic> #include <cassert> +#include <cstddef> + +#include <ableton/link/Optional.hpp> namespace ableton { diff --git a/include/ableton/link/Controller.hpp b/include/ableton/link/Controller.hpp index 99abc9d..ef802f9 100644 --- a/include/ableton/link/Controller.hpp +++ b/include/ableton/link/Controller.hpp @@ -59,8 +59,7 @@ inline ClientState initClientState(const SessionState& sessionState) const auto hostTime = sessionState.ghostXForm.ghostToHost(std::chrono::microseconds{0}); return { Timeline{sessionState.timeline.tempo, sessionState.timeline.beatOrigin, hostTime}, - StartStopState{sessionState.startStopState.isPlaying, - sessionState.startStopState.beats, hostTime}}; + ClientStartStopState{sessionState.startStopState.isPlaying, hostTime, hostTime}}; } inline RtClientState initRtClientState(const ClientState& clientState) @@ -75,36 +74,35 @@ inline RtClientState initRtClientState(const ClientState& clientState) const auto kLocalModGracePeriod = std::chrono::milliseconds(1000); const auto kRtHandlerFallbackPeriod = kLocalModGracePeriod / 2; -inline StartStopState selectPreferredStartStopState( - const StartStopState currentStartStopState, const StartStopState startStopState) +inline ClientStartStopState selectPreferredStartStopState( + const ClientStartStopState currentStartStopState, + const ClientStartStopState startStopState) { return startStopState.timestamp > currentStartStopState.timestamp ? startStopState : currentStartStopState; } -inline StartStopState mapStartStopStateFromSessionToClient( +inline ClientStartStopState mapStartStopStateFromSessionToClient( const StartStopState& sessionStartStopState, - const Timeline& clientTimeline, const Timeline& sessionTimeline, const GhostXForm& xForm) { - const auto clientBeats = clientTimeline.toBeats( - xForm.ghostToHost(sessionTimeline.fromBeats(sessionStartStopState.beats))); - const auto clientTime = xForm.ghostToHost(sessionStartStopState.timestamp); - return StartStopState{sessionStartStopState.isPlaying, clientBeats, clientTime}; + const auto time = + xForm.ghostToHost(sessionTimeline.fromBeats(sessionStartStopState.beats)); + const auto timestamp = xForm.ghostToHost(sessionStartStopState.timestamp); + return ClientStartStopState{sessionStartStopState.isPlaying, time, timestamp}; } inline StartStopState mapStartStopStateFromClientToSession( - const StartStopState& clientStartStopState, - const Timeline& clientTimeline, + const ClientStartStopState& clientStartStopState, const Timeline& sessionTimeline, const GhostXForm& xForm) { - const auto sessionBeats = sessionTimeline.toBeats( - xForm.hostToGhost(clientTimeline.fromBeats(clientStartStopState.beats))); - const auto sessionTime = xForm.hostToGhost(clientStartStopState.timestamp); - return StartStopState{clientStartStopState.isPlaying, sessionBeats, sessionTime}; + const auto sessionBeats = + sessionTimeline.toBeats(xForm.hostToGhost(clientStartStopState.time)); + const auto timestamp = xForm.hostToGhost(clientStartStopState.timestamp); + return StartStopState{clientStartStopState.isPlaying, sessionBeats, timestamp}; } } // namespace detail @@ -120,6 +118,7 @@ template <typename PeerCountCallback, typename TempoCallback, typename StartStopStateCallback, typename Clock, + typename Random, typename IoContext> class Controller { @@ -133,7 +132,7 @@ public: : mTempoCallback(std::move(tempoCallback)) , mStartStopStateCallback(std::move(startStopStateCallback)) , mClock(std::move(clock)) - , mNodeId(NodeId::random()) + , mNodeId(NodeId::random<Random>()) , mSessionId(mNodeId) , mSessionState(detail::initSessionState(tempo, mClock)) , mClientState(detail::initClientState(mSessionState)) @@ -170,6 +169,11 @@ public: Controller& operator=(const Controller&) = delete; Controller& operator=(Controller&&) = delete; + ~Controller() + { + mIo->stop(); + } + void enable(const bool bEnable) { const bool bWasEnabled = mEnabled.exchange(bEnable); @@ -178,6 +182,9 @@ public: mIo->async([this, bEnable] { if (bEnable) { + // Process the pending client states to make sure we don't push one after we + // have joined a running session + mRtClientStateSetter.processPendingClientStates(); // Always reset when first enabling to avoid hijacking // tempo in existing sessions resetState(); @@ -352,8 +359,11 @@ private: mSessionState.ghostXForm)); } - void updateSessionTiming(const Timeline newTimeline, const GhostXForm newXForm) + void updateSessionTiming(Timeline newTimeline, const GhostXForm newXForm) { + // Clamp the session tempo because it may slightly overshoot (999 bpm is + // transferred as 60606 us/beat and received as 999.000999... bpm). + newTimeline = clampTempo(newTimeline); const auto oldTimeline = mSessionState.timeline; const auto oldXForm = mSessionState.ghostXForm; @@ -365,11 +375,20 @@ private: mSessionState.ghostXForm = newXForm; } - // Update the client timeline based on the new session timing data + // Update the client timeline and start stop state based on the new session timing { - std::lock_guard<std::mutex> lock(mClientStateGuard); + std::lock_guard<std::mutex> timelineLock(mClientStateGuard); mClientState.timeline = updateClientTimelineFromSession(mClientState.timeline, mSessionState.timeline, mClock.micros(), mSessionState.ghostXForm); + // Don't pass the start stop state to the client when start stop sync is disabled + // or when we have a default constructed start stop state + if (mStartStopSyncEnabled && mSessionState.startStopState != StartStopState{}) + { + std::lock_guard<std::mutex> startStopStateLock(mSessionStateGuard); + mClientState.startStopState = + detail::mapStartStopStateFromSessionToClient(mSessionState.startStopState, + mSessionState.timeline, mSessionState.ghostXForm); + } } if (oldTimeline.tempo != newTimeline.tempo) @@ -413,9 +432,8 @@ private: { { std::lock_guard<std::mutex> lock(mClientStateGuard); - mClientState.startStopState = - detail::mapStartStopStateFromSessionToClient(startStopState, - mClientState.timeline, mSessionState.timeline, mSessionState.ghostXForm); + mClientState.startStopState = detail::mapStartStopStateFromSessionToClient( + startStopState, mSessionState.timeline, mSessionState.ghostXForm); } invokeStartStopStateCallbackIfChanged(); } @@ -449,7 +467,7 @@ private: std::lock_guard<std::mutex> lock(mClientStateGuard); mSessionState.startStopState = detail::mapStartStopStateFromClientToSession(*clientState.startStopState, - mClientState.timeline, mSessionState.timeline, mSessionState.ghostXForm); + mSessionState.timeline, mSessionState.ghostXForm); mClientState.startStopState = *clientState.startStopState; } @@ -491,9 +509,10 @@ private: const bool sessionIdChanged = mSessionId != session.sessionId; mSessionId = session.sessionId; - // Prevent passing the start stop state of the previous session to the new one. + // Prevent passing the state of the previous session to the new one. if (sessionIdChanged) { + mRtClientStateSetter.processPendingClientStates(); resetSessionStartStopState(); } @@ -510,7 +529,7 @@ private: void resetState() { - mNodeId = NodeId::random(); + mNodeId = NodeId::random<Random>(); mSessionId = mNodeId; const auto xform = detail::initXForm(mClock); @@ -551,7 +570,8 @@ private: RtClientStateSetter(Controller& controller) : mController(controller) , mCallbackDispatcher( - [this] { processPendingClientStates(); }, detail::kRtHandlerFallbackPeriod) + [this] { mController.mIo->async([this]() { processPendingClientStates(); }); }, + detail::kRtHandlerFallbackPeriod) { } @@ -565,6 +585,12 @@ private: return success; } + void processPendingClientStates() + { + const auto clientState = buildMergedPendingClientState(); + mController.handleRtClientState(clientState); + } + private: IncomingClientState buildMergedPendingClientState() { @@ -584,13 +610,6 @@ private: return clientState; } - void processPendingClientStates() - { - const auto clientState = buildMergedPendingClientState(); - mController.mIo->async( - [this, clientState]() { mController.handleRtClientState(clientState); }); - } - Controller& mController; // Assuming a wake up time of one ms for the threads owned by the CallbackDispatcher // and the ioService, buffering 16 client states allows to set eight client states diff --git a/include/ableton/link/Gateway.hpp b/include/ableton/link/Gateway.hpp index e904f25..086abfa 100644 --- a/include/ableton/link/Gateway.hpp +++ b/include/ableton/link/Gateway.hpp @@ -38,13 +38,12 @@ public: NodeState nodeState, GhostXForm ghostXForm, Clock clock) - // TODO: Measurement should have an IoContext injected : mIo(std::move(io)) , mMeasurement(addr, nodeState.sessionId, std::move(ghostXForm), std::move(clock), - util::injectVal(channel(mIo->log(), "gateway@" + addr.to_string()))) + util::injectVal(mIo->clone())) , mPeerGateway(discovery::makeIpV4Gateway(util::injectRef(*mIo), std::move(addr), std::move(observer), @@ -84,7 +83,7 @@ public: private: util::Injected<IoContext> mIo; - MeasurementService<Clock, typename util::Injected<IoContext>::type::Log> mMeasurement; + MeasurementService<Clock, typename std::remove_reference<IoContext>::type> mMeasurement; discovery:: IpV4Gateway<PeerObserver, PeerState, typename util::Injected<IoContext>::type&> mPeerGateway; diff --git a/include/ableton/link/GhostXForm.hpp b/include/ableton/link/GhostXForm.hpp index 3a71a00..2971d7b 100644 --- a/include/ableton/link/GhostXForm.hpp +++ b/include/ableton/link/GhostXForm.hpp @@ -33,12 +33,14 @@ struct GhostXForm { microseconds hostToGhost(const microseconds hostTime) const { - return microseconds{llround(slope * hostTime.count())} + intercept; + return microseconds{llround(slope * static_cast<double>(hostTime.count()))} + + intercept; } microseconds ghostToHost(const microseconds ghostTime) const { - return microseconds{llround((ghostTime - intercept).count() / slope)}; + return microseconds{ + llround(static_cast<double>((ghostTime - intercept).count()) / slope)}; } friend bool operator==(const GhostXForm lhs, const GhostXForm rhs) diff --git a/include/ableton/link/HostTimeFilter.hpp b/include/ableton/link/HostTimeFilter.hpp index d1d3085..2fdbc94 100644 --- a/include/ableton/link/HostTimeFilter.hpp +++ b/include/ableton/link/HostTimeFilter.hpp @@ -28,7 +28,7 @@ namespace ableton namespace link { -template <class T> +template <class Clock> class HostTimeFilter { static const std::size_t kNumPoints = 512; @@ -75,7 +75,7 @@ public: private: std::size_t mIndex; Points mPoints; - T mHostTimeSampler; + Clock mHostTimeSampler; }; } // namespace link diff --git a/include/ableton/link/Kalman.hpp b/include/ableton/link/Kalman.hpp index 4abff9a..06c6387 100644 --- a/include/ableton/link/Kalman.hpp +++ b/include/ableton/link/Kalman.hpp @@ -55,14 +55,14 @@ struct Kalman meanOfDiffs += (mMeasuredValues[k] - mFilterValues[k]); } - meanOfDiffs /= (mVarianceLength); + meanOfDiffs /= static_cast<double>(mVarianceLength); for (size_t i = 0; i < (mVarianceLength); i++) { vVar += (pow(mMeasuredValues[i] - mFilterValues[i] - meanOfDiffs, 2.0)); } - vVar /= (mVarianceLength - 1); + vVar /= static_cast<double>(mVarianceLength - 1); return vVar; } @@ -78,7 +78,7 @@ struct Kalman - mFilterValues[(mCounter - k - 2) % mVarianceLength]); } - meanOfDiffs /= (mVarianceLength); + meanOfDiffs /= static_cast<double>(mVarianceLength); for (size_t i = 0; i < (mVarianceLength); i++) { @@ -87,7 +87,7 @@ struct Kalman 2.0)); } - wVar /= (mVarianceLength - 1); + wVar /= static_cast<double>(mVarianceLength - 1); return wVar; } diff --git a/include/ableton/link/Measurement.hpp b/include/ableton/link/Measurement.hpp index 83bc0d9..c8d0f2b 100644 --- a/include/ableton/link/Measurement.hpp +++ b/include/ableton/link/Measurement.hpp @@ -20,13 +20,12 @@ #pragma once #include <ableton/discovery/Payload.hpp> -#include <ableton/discovery/Socket.hpp> #include <ableton/link/PayloadEntries.hpp> #include <ableton/link/PeerState.hpp> #include <ableton/link/SessionId.hpp> #include <ableton/link/v1/Messages.hpp> -#include <ableton/platforms/asio/AsioService.hpp> #include <ableton/util/Injected.hpp> +#include <ableton/util/SafeAsyncHandler.hpp> #include <chrono> #include <memory> @@ -35,83 +34,57 @@ namespace ableton namespace link { -template <typename IoService, typename Clock, typename Socket, typename Log> +template <typename Clock, typename IoContext> struct Measurement { using Point = std::pair<double, double>; using Callback = std::function<void(std::vector<Point>)>; using Micros = std::chrono::microseconds; - using Timer = typename IoService::Timer; static const std::size_t kNumberDataPoints = 100; static const std::size_t kNumberMeasurements = 5; - Measurement() = default; - Measurement(const PeerState& state, Callback callback, asio::ip::address_v4 address, Clock clock, - util::Injected<Log> log) - : mpIo(new IoService{}) - , mpImpl(std::make_shared<Impl>(*mpIo, - std::move(state), + IoContext io) + : mIo(std::move(io)) + , mpImpl(std::make_shared<Impl>(std::move(state), std::move(callback), std::move(address), std::move(clock), - std::move(log))) + mIo)) { mpImpl->listen(); } - Measurement(Measurement&& rhs) - : mpIo(std::move(rhs.mpIo)) - , mpImpl(std::move(rhs.mpImpl)) - { - } - - ~Measurement() - { - postImplDestruction(); - } - - Measurement& operator=(Measurement&& rhs) - { - postImplDestruction(); - mpIo = std::move(rhs.mpIo); - mpImpl = std::move(rhs.mpImpl); - return *this; - } - - void postImplDestruction() - { - // Post destruction of the impl object into the io thread if valid - if (mpIo) - { - mpIo->post(ImplDeleter{*this}); - } - } + Measurement(const Measurement&) = delete; + Measurement& operator=(Measurement&) = delete; + Measurement(const Measurement&&) = delete; + Measurement& operator=(Measurement&&) = delete; struct Impl : std::enable_shared_from_this<Impl> { - Impl(IoService& io, - const PeerState& state, + using Socket = typename IoContext::template Socket<v1::kMaxMessageSize>; + using Timer = typename IoContext::Timer; + using Log = typename IoContext::Log; + + Impl(const PeerState& state, Callback callback, asio::ip::address_v4 address, Clock clock, - util::Injected<Log> log) - : mpSocket(std::make_shared<Socket>(io)) + IoContext& io) + : mSocket(io.template openUnicastSocket<v1::kMaxMessageSize>(address)) , mSessionId(state.nodeState.sessionId) , mEndpoint(state.endpoint) , mCallback(std::move(callback)) , mClock(std::move(clock)) - , mTimer(util::injectVal(io.makeTimer())) + , mTimer(io.makeTimer()) , mMeasurementsStarted(0) - , mLog(std::move(log)) + , mLog(channel(io.log(), "Measurement on gateway@" + address.to_string())) , mSuccess(false) { - configureUnicastSocket(*mpSocket, address); - const auto ht = HostTime{mClock.micros()}; sendPing(mEndpoint, discovery::makePayload(ht)); resetTimer(); @@ -119,9 +92,9 @@ struct Measurement void resetTimer() { - mTimer->cancel(); - mTimer->expires_from_now(std::chrono::milliseconds(50)); - mTimer->async_wait([this](const typename Timer::ErrorCode e) { + mTimer.cancel(); + mTimer.expires_from_now(std::chrono::milliseconds(50)); + mTimer.async_wait([this](const typename Timer::ErrorCode e) { if (!e) { if (mMeasurementsStarted < kNumberMeasurements) @@ -141,7 +114,7 @@ struct Measurement void listen() { - mpSocket->receive(util::makeAsyncSafe(this->shared_from_this())); + mSocket.receive(util::makeAsyncSafe(this->shared_from_this())); } // Operator to handle incoming messages on the interface @@ -156,7 +129,7 @@ struct Measurement if (header.messageType == v1::kPong) { - debug(*mLog) << "Received Pong message from " << from; + debug(mLog) << "Received Pong message from " << from; // parse for all entries SessionId sessionId{}; @@ -175,7 +148,7 @@ struct Measurement } catch (const std::runtime_error& err) { - warning(*mLog) << "Failed parsing payload, caught exception: " << err.what(); + warning(mLog) << "Failed parsing payload, caught exception: " << err.what(); listen(); return; } @@ -215,7 +188,7 @@ struct Measurement } else { - debug(*mLog) << "Received invalid message from " << from; + debug(mLog) << "Received invalid message from " << from; listen(); } } @@ -230,40 +203,40 @@ struct Measurement try { - mpSocket->send(buffer.data(), numBytes, to); + mSocket.send(buffer.data(), numBytes, to); } catch (const std::runtime_error& err) { - info(*mLog) << "Failed to send Ping to " << to.address().to_string() << ": " - << err.what(); + info(mLog) << "Failed to send Ping to " << to.address().to_string() << ": " + << err.what(); } } void finish() { - mTimer->cancel(); + mTimer.cancel(); mCallback(std::move(mData)); mData = {}; mSuccess = true; - debug(*mLog) << "Measuring " << mEndpoint << " done."; + debug(mLog) << "Measuring " << mEndpoint << " done."; } void fail() { mCallback(std::vector<Point>{}); mData = {}; - debug(*mLog) << "Measuring " << mEndpoint << " failed."; + debug(mLog) << "Measuring " << mEndpoint << " failed."; } - std::shared_ptr<Socket> mpSocket; + Socket mSocket; SessionId mSessionId; asio::ip::udp::endpoint mEndpoint; std::vector<std::pair<double, double>> mData; Callback mCallback; Clock mClock; - util::Injected<typename IoService::Timer> mTimer; + Timer mTimer; std::size_t mMeasurementsStarted; - util::Injected<Log> mLog; + Log mLog; bool mSuccess; }; @@ -288,7 +261,7 @@ struct Measurement std::shared_ptr<Impl> mpImpl; }; - std::unique_ptr<IoService> mpIo; + IoContext mIo; std::shared_ptr<Impl> mpImpl; }; diff --git a/include/ableton/link/MeasurementEndpointV4.hpp b/include/ableton/link/MeasurementEndpointV4.hpp index 2fdd3ec..a7551b3 100644 --- a/include/ableton/link/MeasurementEndpointV4.hpp +++ b/include/ableton/link/MeasurementEndpointV4.hpp @@ -53,12 +53,12 @@ struct MeasurementEndpointV4 { using namespace std; auto addrRes = - discovery::Deserialize<std::uint32_t>::fromNetworkByteStream(move(begin), end); + discovery::Deserialize<std::uint32_t>::fromNetworkByteStream(std::move(begin), end); auto portRes = discovery::Deserialize<std::uint16_t>::fromNetworkByteStream( - move(addrRes.second), end); - return make_pair(MeasurementEndpointV4{{asio::ip::address_v4{move(addrRes.first)}, - move(portRes.first)}}, - move(portRes.second)); + std::move(addrRes.second), end); + return make_pair(MeasurementEndpointV4{{asio::ip::address_v4{std::move(addrRes.first)}, + std::move(portRes.first)}}, + std::move(portRes.second)); } asio::ip::udp::endpoint ep; diff --git a/include/ableton/link/MeasurementService.hpp b/include/ableton/link/MeasurementService.hpp index 58c7075..f590715 100644 --- a/include/ableton/link/MeasurementService.hpp +++ b/include/ableton/link/MeasurementService.hpp @@ -19,7 +19,6 @@ #pragma once -#include <ableton/discovery/Socket.hpp> #include <ableton/link/GhostXForm.hpp> #include <ableton/link/Kalman.hpp> #include <ableton/link/LinearRegression.hpp> @@ -28,48 +27,34 @@ #include <ableton/link/PingResponder.hpp> #include <ableton/link/SessionId.hpp> #include <ableton/link/v1/Messages.hpp> -#include <ableton/platforms/asio/AsioService.hpp> -#include <ableton/util/Log.hpp> #include <map> #include <memory> -#include <thread> namespace ableton { namespace link { -template <typename Clock, typename Log> +template <typename Clock, typename IoContext> class MeasurementService { public: + using IoType = util::Injected<IoContext>; using Point = std::pair<double, double>; - - using MeasurementInstance = Measurement<platforms::asio::AsioService, - Clock, - discovery::Socket<v1::kMaxMessageSize>, - Log>; - - using MeasurementServicePingResponder = PingResponder<platforms::asio::AsioService&, - Clock, - discovery::Socket<v1::kMaxMessageSize>, - Log>; - - static const std::size_t kNumberThreads = 1; + using MeasurementInstance = Measurement<Clock, IoContext>; MeasurementService(asio::ip::address_v4 address, SessionId sessionId, GhostXForm ghostXForm, Clock clock, - util::Injected<Log> log) + IoType io) : mClock(std::move(clock)) - , mLog(std::move(log)) + , mIo(std::move(io)) , mPingResponder(std::move(address), std::move(sessionId), std::move(ghostXForm), - util::injectRef(mIo), mClock, - mLog) + util::injectRef(*mIo)) { } @@ -78,10 +63,10 @@ public: ~MeasurementService() { - // Clear the measurement map in the io service so that whatever + // Clear the measurement map in the IoContext so that whatever // cleanup code executes in response to the destruction of the - // measurement objects still have access to the io service - mIo.post([this] { mMeasurementMap.clear(); }); + // measurement objects still have access to the IoContext. + mIo->async([this] { mMeasurementMap.clear(); }); } void updateNodeState(const SessionId& sessionId, const GhostXForm& xform) @@ -100,19 +85,22 @@ public: { using namespace std; - mIo.post([this, state, handler] { + mIo->async([this, state, handler] { const auto nodeId = state.nodeState.nodeId; auto addr = mPingResponder.endpoint().address().to_v4(); auto callback = CompletionCallback<Handler>{*this, nodeId, handler}; try { + mMeasurementMap[nodeId] = - MeasurementInstance{state, move(callback), move(addr), mClock, mLog}; + std::unique_ptr<MeasurementInstance>(new MeasurementInstance{ + state, std::move(callback), std::move(addr), mClock, mIo->clone()}); } catch (const runtime_error& err) { - info(*mLog) << "Failed to measure. Reason: " << err.what(); + info(mIo->log()) << "gateway@" + addr.to_string() + << " Failed to measure. Reason: " << err.what(); handler(GhostXForm{}); } }); @@ -142,14 +130,14 @@ private: using namespace std; using std::chrono::microseconds; - // Post this to the measurement service's io service so that we + // Post this to the measurement service's IoContext so that we // don't delete the measurement object in its stack. Capture all // needed data separately from this, since this object may be // gone by the time the block gets executed. auto nodeId = mNodeId; auto handler = mHandler; - auto& measurementMap = mService.mMeasurementMap; - mService.mIo.post([nodeId, handler, &measurementMap, data] { + auto& measurementMap = mMeasurementService.mMeasurementMap; + mMeasurementService.mIo->async([nodeId, handler, &measurementMap, data] { const auto it = measurementMap.find(nodeId); if (it != measurementMap.end()) { @@ -166,20 +154,19 @@ private: }); } - MeasurementService& mService; + MeasurementService& mMeasurementService; NodeId mNodeId; Handler mHandler; }; - // Make sure the measurement map outlives the io service so that the rest of + // Make sure the measurement map outlives the IoContext so that the rest of // the members are guaranteed to be valid when any final handlers // are begin run. - using MeasurementMap = std::map<NodeId, MeasurementInstance>; + using MeasurementMap = std::map<NodeId, std::unique_ptr<MeasurementInstance>>; MeasurementMap mMeasurementMap; Clock mClock; - util::Injected<Log> mLog; - platforms::asio::AsioService mIo; - MeasurementServicePingResponder mPingResponder; + IoType mIo; + PingResponder<Clock, IoContext> mPingResponder; }; } // namespace link diff --git a/include/ableton/link/NodeId.hpp b/include/ableton/link/NodeId.hpp index c99899a..5076320 100644 --- a/include/ableton/link/NodeId.hpp +++ b/include/ableton/link/NodeId.hpp @@ -23,7 +23,6 @@ #include <algorithm> #include <array> #include <cstdint> -#include <random> #include <string> namespace ableton @@ -42,18 +41,15 @@ struct NodeId : NodeIdArray { } + template <typename Random> static NodeId random() { using namespace std; + NodeId nodeId; - random_device rd; - mt19937 gen(rd()); - // uint8_t not standardized for this type - use unsigned - uniform_int_distribution<unsigned> dist(33, 126); // printable ascii chars + Random random; + generate(nodeId.begin(), nodeId.end(), [&] { return random(); }); - NodeId nodeId; - generate( - nodeId.begin(), nodeId.end(), [&] { return static_cast<uint8_t>(dist(gen)); }); return nodeId; } @@ -73,8 +69,8 @@ struct NodeId : NodeIdArray { using namespace std; auto result = - discovery::Deserialize<NodeIdArray>::fromNetworkByteStream(move(begin), move(end)); - return make_pair(NodeId(move(result.first)), move(result.second)); + discovery::Deserialize<NodeIdArray>::fromNetworkByteStream(std::move(begin), std::move(end)); + return make_pair(NodeId(std::move(result.first)), std::move(result.second)); } }; diff --git a/include/ableton/link/NodeState.hpp b/include/ableton/link/NodeState.hpp index c7b9961..c7d8178 100644 --- a/include/ableton/link/NodeState.hpp +++ b/include/ableton/link/NodeState.hpp @@ -56,13 +56,13 @@ struct NodeState static NodeState fromPayload(NodeId nodeId, It begin, It end) { using namespace std; - auto nodeState = NodeState{move(nodeId), {}, {}, {}}; - discovery::parsePayload<Timeline, SessionMembership, StartStopState>(move(begin), - move(end), [&nodeState](Timeline tl) { nodeState.timeline = move(tl); }, + auto nodeState = NodeState{std::move(nodeId), {}, {}, {}}; + discovery::parsePayload<Timeline, SessionMembership, StartStopState>(std::move(begin), + std::move(end), [&nodeState](Timeline tl) { nodeState.timeline = std::move(tl); }, [&nodeState](SessionMembership membership) { - nodeState.sessionId = move(membership.sessionId); + nodeState.sessionId = std::move(membership.sessionId); }, - [&nodeState](StartStopState ststst) { nodeState.startStopState = move(ststst); }); + [&nodeState](StartStopState ststst) { nodeState.startStopState = std::move(ststst); }); return nodeState; } diff --git a/include/ableton/link/PayloadEntries.hpp b/include/ableton/link/PayloadEntries.hpp index bc5a458..cefd35f 100644 --- a/include/ableton/link/PayloadEntries.hpp +++ b/include/ableton/link/PayloadEntries.hpp @@ -57,8 +57,8 @@ struct HostTime { using namespace std; auto result = discovery::Deserialize<chrono::microseconds>::fromNetworkByteStream( - move(begin), move(end)); - return make_pair(HostTime{move(result.first)}, move(result.second)); + std::move(begin), std::move(end)); + return make_pair(HostTime{std::move(result.first)}, std::move(result.second)); } std::chrono::microseconds time; @@ -93,8 +93,8 @@ struct GHostTime { using namespace std; auto result = discovery::Deserialize<chrono::microseconds>::fromNetworkByteStream( - move(begin), move(end)); - return make_pair(GHostTime{move(result.first)}, move(result.second)); + std::move(begin), std::move(end)); + return make_pair(GHostTime{std::move(result.first)}, std::move(result.second)); } std::chrono::microseconds time; @@ -129,8 +129,8 @@ struct PrevGHostTime { using namespace std; auto result = discovery::Deserialize<chrono::microseconds>::fromNetworkByteStream( - move(begin), move(end)); - return make_pair(PrevGHostTime{move(result.first)}, move(result.second)); + std::move(begin), std::move(end)); + return make_pair(PrevGHostTime{std::move(result.first)}, std::move(result.second)); } std::chrono::microseconds time; diff --git a/include/ableton/link/PeerState.hpp b/include/ableton/link/PeerState.hpp index 8b58df4..3bbfbc0 100644 --- a/include/ableton/link/PeerState.hpp +++ b/include/ableton/link/PeerState.hpp @@ -73,10 +73,10 @@ struct PeerState static PeerState fromPayload(NodeId id, It begin, It end) { using namespace std; - auto peerState = PeerState{NodeState::fromPayload(move(id), begin, end), {}}; + auto peerState = PeerState{NodeState::fromPayload(std::move(id), begin, end), {}}; - discovery::parsePayload<MeasurementEndpointV4>(move(begin), move(end), - [&peerState](MeasurementEndpointV4 me4) { peerState.endpoint = move(me4.ep); }); + discovery::parsePayload<MeasurementEndpointV4>(std::move(begin), std::move(end), + [&peerState](MeasurementEndpointV4 me4) { peerState.endpoint = std::move(me4.ep); }); return peerState; } diff --git a/include/ableton/link/Peers.hpp b/include/ableton/link/Peers.hpp index 24172b3..ce674c9 100644 --- a/include/ableton/link/Peers.hpp +++ b/include/ableton/link/Peers.hpp @@ -225,14 +225,14 @@ private: isNewSessionStartStopState = !sessionStartStopStateExists(peerSession, peerStartStopState); - auto peer = make_pair(move(peerState), move(gatewayAddr)); + auto peer = make_pair(std::move(peerState), std::move(gatewayAddr)); const auto idRange = equal_range(begin(mPeers), end(mPeers), peer, PeerIdComp{}); if (idRange.first == idRange.second) { // This peer is not currently known on any gateway didSessionMembershipChange = true; - mPeers.insert(move(idRange.first), move(peer)); + mPeers.insert(std::move(idRange.first), std::move(peer)); } else { @@ -249,12 +249,12 @@ private: if (addrRange.first == addrRange.second) { // First time on this gateway, add it - mPeers.insert(move(addrRange.first), move(peer)); + mPeers.insert(std::move(addrRange.first), std::move(peer)); } else { // We have an entry for this peer on this gateway, update it - *addrRange.first = move(peer); + *addrRange.first = std::move(peer); } } } // end lock @@ -292,7 +292,7 @@ private: if (it != end(mPeers)) { - mPeers.erase(move(it)); + mPeers.erase(std::move(it)); didSessionMembershipChange = true; } } // end lock diff --git a/include/ableton/link/PingResponder.hpp b/include/ableton/link/PingResponder.hpp index 0650cce..e8a5e8b 100644 --- a/include/ableton/link/PingResponder.hpp +++ b/include/ableton/link/PingResponder.hpp @@ -23,35 +23,34 @@ #include <ableton/link/PayloadEntries.hpp> #include <ableton/link/SessionId.hpp> #include <ableton/link/v1/Messages.hpp> -#include <ableton/platforms/asio/AsioWrapper.hpp> #include <ableton/util/Injected.hpp> #include <ableton/util/SafeAsyncHandler.hpp> #include <chrono> #include <memory> -#include <thread> namespace ableton { namespace link { -template <typename Io, typename Clock, typename Socket, typename Log> +template <typename Clock, typename IoContext> class PingResponder { + using IoType = util::Injected<IoContext&>; + using Socket = typename IoType::type::template Socket<v1::kMaxMessageSize>; + public: PingResponder(asio::ip::address_v4 address, SessionId sessionId, GhostXForm ghostXForm, - util::Injected<Io> io, Clock clock, - util::Injected<Log> log) - : mIo(std::move(io)) - , mpImpl(std::make_shared<Impl>(*mIo, - std::move(address), + IoType io) + : mIo(io) + , mpImpl(std::make_shared<Impl>(std::move(address), std::move(sessionId), std::move(ghostXForm), std::move(clock), - std::move(log))) + std::move(io))) { mpImpl->listen(); } @@ -61,16 +60,16 @@ public: ~PingResponder() { - // post the release of the impl object into the io service so that + // post the release of the impl object into the IoContext so that // it happens in the same thread as its handlers auto pImpl = mpImpl; - mIo->post([pImpl]() mutable { pImpl.reset(); }); + mIo->async([pImpl]() mutable { pImpl.reset(); }); } void updateNodeState(const SessionId& sessionId, const GhostXForm& xform) { auto pImpl = mpImpl; - mIo->post([pImpl, sessionId, xform] { + mIo->async([pImpl, sessionId, xform] { pImpl->mSessionId = std::move(sessionId); pImpl->mGhostXForm = std::move(xform); }); @@ -94,19 +93,17 @@ public: private: struct Impl : std::enable_shared_from_this<Impl> { - Impl(typename util::Injected<Io>::type& io, - asio::ip::address_v4 address, + Impl(asio::ip::address_v4 address, SessionId sessionId, GhostXForm ghostXForm, Clock clock, - util::Injected<Log> log) + IoType io) : mSessionId(std::move(sessionId)) , mGhostXForm(std::move(ghostXForm)) , mClock(std::move(clock)) - , mLog(std::move(log)) - , mSocket(io) + , mLog(channel(io->log(), "gateway@" + address.to_string())) + , mSocket(io->template openUnicastSocket<v1::kMaxMessageSize>(address)) { - configureUnicastSocket(mSocket, address); } void listen() @@ -131,7 +128,7 @@ private: sizeInByteStream(makePayload(HostTime{}, PrevGHostTime{})); if (header.messageType == v1::kPing && payloadSize <= maxPayloadSize) { - debug(*mLog) << "Received ping message from " << from; + debug(mLog) << " Received ping message from " << from; try { @@ -139,12 +136,12 @@ private: } catch (const std::runtime_error& err) { - info(*mLog) << "Failed to send pong to " << from << ". Reason: " << err.what(); + info(mLog) << " Failed to send pong to " << from << ". Reason: " << err.what(); } } else { - info(*mLog) << "Received invalid Message from " << from << "."; + info(mLog) << " Received invalid Message from " << from << "."; } listen(); } @@ -173,11 +170,11 @@ private: SessionId mSessionId; GhostXForm mGhostXForm; Clock mClock; - util::Injected<Log> mLog; + typename IoType::type::Log mLog; Socket mSocket; }; - util::Injected<Io> mIo; + IoType mIo; std::shared_ptr<Impl> mpImpl; }; diff --git a/include/ableton/link/SessionId.hpp b/include/ableton/link/SessionId.hpp index ac89672..7881e20 100644 --- a/include/ableton/link/SessionId.hpp +++ b/include/ableton/link/SessionId.hpp @@ -52,8 +52,8 @@ struct SessionMembership static std::pair<SessionMembership, It> fromNetworkByteStream(It begin, It end) { using namespace std; - auto idRes = SessionId::fromNetworkByteStream(move(begin), move(end)); - return make_pair(SessionMembership{move(idRes.first)}, move(idRes.second)); + auto idRes = SessionId::fromNetworkByteStream(std::move(begin), std::move(end)); + return make_pair(SessionMembership{std::move(idRes.first)}, std::move(idRes.second)); } SessionId sessionId; diff --git a/include/ableton/link/SessionState.hpp b/include/ableton/link/SessionState.hpp index f64325f..67e2dd6 100644 --- a/include/ableton/link/SessionState.hpp +++ b/include/ableton/link/SessionState.hpp @@ -30,6 +30,7 @@ namespace link using OptionalTimeline = Optional<Timeline>; using OptionalStartStopState = Optional<StartStopState>; +using OptionalClientStartStopState = Optional<ClientStartStopState>; struct SessionState { @@ -52,13 +53,13 @@ struct ClientState } Timeline timeline; - StartStopState startStopState; + ClientStartStopState startStopState; }; struct RtClientState { Timeline timeline; - StartStopState startStopState; + ClientStartStopState startStopState; std::chrono::microseconds timelineTimestamp; std::chrono::microseconds startStopStateTimestamp; }; @@ -66,7 +67,7 @@ struct RtClientState struct IncomingClientState { OptionalTimeline timeline; - OptionalStartStopState startStopState; + OptionalClientStartStopState startStopState; std::chrono::microseconds timelineTimestamp; }; diff --git a/include/ableton/link/Sessions.hpp b/include/ableton/link/Sessions.hpp index fc82bdd..ee1a95e 100644 --- a/include/ableton/link/Sessions.hpp +++ b/include/ableton/link/Sessions.hpp @@ -86,11 +86,11 @@ public: if (sid == mCurrent.sessionId) { // matches our current session, update the timeline if necessary - updateTimeline(mCurrent, move(timeline)); + updateTimeline(mCurrent, std::move(timeline)); } else { - auto session = Session{move(sid), move(timeline), {}}; + auto session = Session{std::move(sid), std::move(timeline), {}}; const auto range = equal_range(begin(mOtherSessions), end(mOtherSessions), session, SessionIdComp{}); if (range.first == range.second) @@ -98,12 +98,12 @@ public: // brand new session, insert it into our list of known // sessions and launch a measurement launchSessionMeasurement(session); - mOtherSessions.insert(range.first, move(session)); + mOtherSessions.insert(range.first, std::move(session)); } else { // we've seen this session before, update its timeline if necessary - updateTimeline(*range.first, move(timeline)); + updateTimeline(*range.first, std::move(timeline)); } } return mCurrent.timeline; @@ -125,7 +125,7 @@ private: // mark that a session is in progress by clearing out the // session's timestamp session.measurement.timestamp = {}; - mMeasure(move(peer), MeasurementResultsHandler{*this, session.sessionId}); + mMeasure(std::move(peer), MeasurementResultsHandler{*this, session.sessionId}); } } @@ -136,11 +136,11 @@ private: debug(mIo->log()) << "Session " << id << " measurement completed with result " << "(" << xform.slope << ", " << xform.intercept.count() << ")"; - auto measurement = SessionMeasurement{move(xform), mClock.micros()}; + auto measurement = SessionMeasurement{std::move(xform), mClock.micros()}; if (mCurrent.sessionId == id) { - mCurrent.measurement = move(measurement); + mCurrent.measurement = std::move(measurement); mCallback(mCurrent); } else @@ -156,7 +156,7 @@ private: const auto curGhost = mCurrent.measurement.xform.hostToGhost(hostTime); const auto newGhost = measurement.xform.hostToGhost(hostTime); // update the measurement for the session entry - range.first->measurement = move(measurement); + range.first->measurement = std::move(measurement); // If session times too close - fall back to session id order const auto ghostDiff = newGhost - curGhost; if (ghostDiff > SESSION_EPS @@ -165,13 +165,13 @@ private: { // The new session wins, switch over to it auto current = mCurrent; - mCurrent = move(*range.first); + mCurrent = std::move(*range.first); mOtherSessions.erase(range.first); // Put the old current session back into our list of known // sessions so that we won't re-measure it const auto it = upper_bound( begin(mOtherSessions), end(mOtherSessions), current, SessionIdComp{}); - mOtherSessions.insert(it, move(current)); + mOtherSessions.insert(it, std::move(current)); // And notify that we have a new session and make sure that // we remeasure it periodically. mCallback(mCurrent); @@ -297,7 +297,7 @@ Sessions<Peers, MeasurePeer, JoinSessionCallback, IoContext, Clock> makeSessions Clock clock) { using namespace std; - return {move(init), move(peers), move(measure), move(join), move(io), move(clock)}; + return {std::move(init), std::move(peers), std::move(measure), std::move(join), std::move(io), std::move(clock)}; } } // namespace link diff --git a/include/ableton/link/StartStopState.hpp b/include/ableton/link/StartStopState.hpp index 74654ef..38fba5a 100644 --- a/include/ableton/link/StartStopState.hpp +++ b/include/ableton/link/StartStopState.hpp @@ -79,10 +79,10 @@ struct StartStopState using namespace std; using namespace discovery; auto result = - Deserialize<StartStopStateTuple>::fromNetworkByteStream(move(begin), move(end)); + Deserialize<StartStopStateTuple>::fromNetworkByteStream(std::move(begin), std::move(end)); auto state = StartStopState{get<0>(result.first), get<1>(result.first), get<2>(result.first)}; - return make_pair(move(state), move(result.second)); + return make_pair(std::move(state), std::move(result.second)); } bool isPlaying{false}; @@ -96,6 +96,35 @@ private: } }; +struct ClientStartStopState +{ + ClientStartStopState() = default; + + ClientStartStopState(const bool aIsPlaying, + const std::chrono::microseconds aTime, + const std::chrono::microseconds aTimestamp) + : isPlaying(aIsPlaying) + , time(aTime) + , timestamp(aTimestamp) + { + } + + friend bool operator==(const ClientStartStopState& lhs, const ClientStartStopState& rhs) + { + return std::tie(lhs.isPlaying, lhs.time, lhs.timestamp) + == std::tie(rhs.isPlaying, rhs.time, rhs.timestamp); + } + + friend bool operator!=(const ClientStartStopState& lhs, const ClientStartStopState& rhs) + { + return !(lhs == rhs); + } + + bool isPlaying{false}; + std::chrono::microseconds time{0}; + std::chrono::microseconds timestamp{0}; +}; + struct ApiStartStopState { ApiStartStopState() = default; diff --git a/include/ableton/link/Tempo.hpp b/include/ableton/link/Tempo.hpp index 2557928..69beb9c 100644 --- a/include/ableton/link/Tempo.hpp +++ b/include/ableton/link/Tempo.hpp @@ -27,41 +27,43 @@ namespace ableton namespace link { -struct Tempo : std::tuple<double> +struct Tempo { Tempo() = default; // Beats per minute explicit Tempo(const double bpm) - : std::tuple<double>(bpm) + : mValue(bpm) { } Tempo(const std::chrono::microseconds microsPerBeat) - : std::tuple<double>(60. * 1e6 / microsPerBeat.count()) + : mValue(60. * 1e6 / static_cast<double>(microsPerBeat.count())) { } double bpm() const { - return std::get<0>(*this); + return mValue; } std::chrono::microseconds microsPerBeat() const { - return std::chrono::microseconds{llround(60. * 1e6 / bpm())}; + return std::chrono::microseconds{std::llround(60. * 1e6 / bpm())}; } // Given the tempo, convert a time to a beat value Beats microsToBeats(const std::chrono::microseconds micros) const { - return Beats{micros.count() / static_cast<double>(microsPerBeat().count())}; + return Beats{ + static_cast<double>(micros.count()) / static_cast<double>(microsPerBeat().count())}; } // Given the tempo, convert a beat to a time value std::chrono::microseconds beatsToMicros(const Beats beats) const { - return std::chrono::microseconds{llround(beats.floating() * microsPerBeat().count())}; + return std::chrono::microseconds{ + std::llround(beats.floating() * static_cast<double>(microsPerBeat().count()))}; } // Model the NetworkByteStreamSerializable concept @@ -84,6 +86,39 @@ struct Tempo : std::tuple<double> std::move(begin), std::move(end)); return std::make_pair(Tempo{std::move(result.first)}, std::move(result.second)); } + + friend bool operator==(const Tempo lhs, const Tempo rhs) + { + return lhs.mValue == rhs.mValue; + } + + friend bool operator!=(const Tempo lhs, const Tempo rhs) + { + return lhs.mValue != rhs.mValue; + } + + friend bool operator<(const Tempo lhs, const Tempo rhs) + { + return lhs.mValue < rhs.mValue; + } + + friend bool operator>(const Tempo lhs, const Tempo rhs) + { + return lhs.mValue > rhs.mValue; + } + + friend bool operator<=(const Tempo lhs, const Tempo rhs) + { + return lhs.mValue <= rhs.mValue; + } + + friend bool operator>=(const Tempo lhs, const Tempo rhs) + { + return lhs.mValue >= rhs.mValue; + } + +private: + double mValue = 0; }; } // namespace link diff --git a/include/ableton/link/Timeline.hpp b/include/ableton/link/Timeline.hpp index f0f084e..941f943 100644 --- a/include/ableton/link/Timeline.hpp +++ b/include/ableton/link/Timeline.hpp @@ -83,9 +83,9 @@ struct Timeline Timeline timeline; auto result = Deserialize<tuple<Tempo, Beats, chrono::microseconds>>::fromNetworkByteStream( - move(begin), move(end)); - tie(timeline.tempo, timeline.beatOrigin, timeline.timeOrigin) = move(result.first); - return make_pair(move(timeline), move(result.second)); + std::move(begin), std::move(end)); + tie(timeline.tempo, timeline.beatOrigin, timeline.timeOrigin) = std::move(result.first); + return make_pair(std::move(timeline), std::move(result.second)); } Tempo tempo; diff --git a/include/ableton/link/v1/Messages.hpp b/include/ableton/link/v1/Messages.hpp index b1e864b..3844b40 100644 --- a/include/ableton/link/v1/Messages.hpp +++ b/include/ableton/link/v1/Messages.hpp @@ -88,7 +88,7 @@ It encodeMessage(const MessageType messageType, const Payload& payload, It out) { return toNetworkByteStream( payload, toNetworkByteStream( - header, copy(begin(kProtocolHeader), end(kProtocolHeader), move(out)))); + header, copy(begin(kProtocolHeader), end(kProtocolHeader), std::move(out)))); } else { diff --git a/include/ableton/platforms/Config.hpp b/include/ableton/platforms/Config.hpp index cd1506c..63aea27 100644 --- a/include/ableton/platforms/Config.hpp +++ b/include/ableton/platforms/Config.hpp @@ -24,16 +24,24 @@ #if defined(LINK_PLATFORM_WINDOWS) #include <ableton/platforms/asio/Context.hpp> +#include <ableton/platforms/stl/Random.hpp> #include <ableton/platforms/windows/Clock.hpp> #include <ableton/platforms/windows/ScanIpIfAddrs.hpp> #elif defined(LINK_PLATFORM_MACOSX) #include <ableton/platforms/asio/Context.hpp> #include <ableton/platforms/darwin/Clock.hpp> #include <ableton/platforms/posix/ScanIpIfAddrs.hpp> +#include <ableton/platforms/stl/Random.hpp> #elif defined(LINK_PLATFORM_LINUX) #include <ableton/platforms/asio/Context.hpp> #include <ableton/platforms/linux/Clock.hpp> #include <ableton/platforms/posix/ScanIpIfAddrs.hpp> +#include <ableton/platforms/stl/Random.hpp> +#elif defined(ESP_PLATFORM) +#include <ableton/platforms/esp32/Clock.hpp> +#include <ableton/platforms/esp32/Context.hpp> +#include <ableton/platforms/esp32/Random.hpp> +#include <ableton/platforms/esp32/ScanIpIfAddrs.hpp> #endif namespace ableton @@ -47,20 +55,26 @@ namespace platform using Clock = platforms::windows::Clock; using IoContext = platforms::asio::Context<platforms::windows::ScanIpIfAddrs, util::NullLog>; +using Random = platforms::stl::Random; #elif defined(LINK_PLATFORM_MACOSX) using Clock = platforms::darwin::Clock; using IoContext = platforms::asio::Context<platforms::posix::ScanIpIfAddrs, util::NullLog>; +using Random = platforms::stl::Random; #elif defined(LINK_PLATFORM_LINUX) -using Clock = platforms::linux::ClockMonotonic; +using Clock = platforms::linux::ClockMonotonicRaw; using IoContext = platforms::asio::Context<platforms::posix::ScanIpIfAddrs, util::NullLog>; -#endif +using Random = platforms::stl::Random; -using Controller = - Controller<PeerCountCallback, TempoCallback, StartStopStateCallback, Clock, IoContext>; +#elif defined(ESP_PLATFORM) +using Clock = platforms::esp32::Clock; +using IoContext = + platforms::esp32::Context<platforms::esp32::ScanIpIfAddrs, util::NullLog>; +using Random = platforms::esp32::Random; +#endif } // namespace platform } // namespace link diff --git a/include/ableton/platforms/asio/AsioService.hpp b/include/ableton/platforms/asio/AsioService.hpp deleted file mode 100644 index c33b892..0000000 --- a/include/ableton/platforms/asio/AsioService.hpp +++ /dev/null @@ -1,105 +0,0 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - * - * If you would like to incorporate Link into a proprietary software application, - * please contact <link-devs@ableton.com>. - */ - -#pragma once - -#include <ableton/platforms/asio/AsioTimer.hpp> -#include <ableton/platforms/asio/AsioWrapper.hpp> -#include <thread> - -namespace ableton -{ -namespace platforms -{ -namespace asio -{ - -class AsioService -{ -public: - using Timer = AsioTimer; - - AsioService() - : AsioService(DefaultHandler{}) - { - } - - template <typename ExceptionHandler> - explicit AsioService(ExceptionHandler exceptHandler) - : mpWork(new ::asio::io_service::work(mService)) - { - mThread = - std::thread{[](::asio::io_service& service, ExceptionHandler handler) { - for (;;) - { - try - { - service.run(); - break; - } - catch (const typename ExceptionHandler::Exception& exception) - { - handler(exception); - } - } - }, - std::ref(mService), std::move(exceptHandler)}; - } - - ~AsioService() - { - mpWork.reset(); - mThread.join(); - } - - AsioTimer makeTimer() - { - return {mService}; - } - - template <typename Handler> - void post(Handler handler) - { - mService.post(std::move(handler)); - } - - ::asio::io_service mService; - -private: - // Default handler is hidden and defines a hidden exception type - // that will never be thrown by other code, so it effectively does - // not catch. - struct DefaultHandler - { - struct Exception - { - }; - - void operator()(const Exception&) - { - } - }; - - std::unique_ptr<::asio::io_service::work> mpWork; - std::thread mThread; -}; - -} // namespace asio -} // namespace platforms -} // namespace ableton diff --git a/include/ableton/platforms/asio/AsioWrapper.hpp b/include/ableton/platforms/asio/AsioWrapper.hpp index 02ae71f..6477e45 100644 --- a/include/ableton/platforms/asio/AsioWrapper.hpp +++ b/include/ableton/platforms/asio/AsioWrapper.hpp @@ -26,11 +26,13 @@ * by Link. */ +#if !defined(ESP_PLATFORM) #pragma push_macro("ASIO_STANDALONE") #define ASIO_STANDALONE 1 #pragma push_macro("ASIO_NO_TYPEID") #define ASIO_NO_TYPEID 1 +#endif #if defined(LINK_PLATFORM_WINDOWS) #pragma push_macro("INCL_EXTRA_HTON_FUNCTIONS") @@ -48,6 +50,9 @@ #if __has_warning("-Wcomma") #pragma clang diagnostic ignored "-Wcomma" #endif +#if __has_warning("-Wshorten-64-to-32") +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif #if __has_warning("-Wunused-local-typedef") #pragma clang diagnostic ignored "-Wunused-local-typedef" #endif @@ -58,6 +63,7 @@ #pragma warning(push, 0) #pragma warning(disable : 4242) #pragma warning(disable : 4702) +#pragma warning(disable : 5204) #endif #include <asio.hpp> @@ -67,8 +73,10 @@ #pragma pop_macro("INCL_EXTRA_HTON_FUNCTIONS") #endif +#if !defined(ESP_PLATFORM) #pragma pop_macro("ASIO_STANDALONE") #pragma pop_macro("ASIO_NO_TYPEID") +#endif #if defined(_MSC_VER) #pragma warning(pop) diff --git a/include/ableton/platforms/asio/Context.hpp b/include/ableton/platforms/asio/Context.hpp index ac30e5d..54125fc 100644 --- a/include/ableton/platforms/asio/Context.hpp +++ b/include/ableton/platforms/asio/Context.hpp @@ -87,13 +87,24 @@ public: ~Context() { - if (mpService) + if (mpService && mpWork) { mpWork.reset(); mThread.join(); } } + void stop() + { + if (mpService && mpWork) + { + mpWork.reset(); + mpService->stop(); + mThread.join(); + } + } + + template <std::size_t BufferSize> Socket<BufferSize> openUnicastSocket(const ::asio::ip::address_v4& addr) { diff --git a/include/ableton/platforms/asio/LockFreeCallbackDispatcher.hpp b/include/ableton/platforms/asio/LockFreeCallbackDispatcher.hpp index a1e85df..4515eb6 100644 --- a/include/ableton/platforms/asio/LockFreeCallbackDispatcher.hpp +++ b/include/ableton/platforms/asio/LockFreeCallbackDispatcher.hpp @@ -27,13 +27,15 @@ namespace ableton { namespace platforms { +namespace asio +{ // Utility to signal invocation of a callback on another thread in a lock free manner. // The callback is evoked on a thread owned by the instance of this class. // // A condition variable is used to notify a waiting thread, but only if the required // lock can be acquired immediately. If that fails, we fall back on signaling -// after a timeout. This gives us a guaranteed minimum signalling rate which is defined +// after a timeout. This gives us a guaranteed minimum signaling rate which is defined // by the fallbackPeriod parameter. template <typename Callback, typename Duration> @@ -85,5 +87,6 @@ private: std::thread mThread; }; +} // namespace asio } // namespace platforms } // namespace ableton diff --git a/include/ableton/platforms/esp32/Clock.hpp b/include/ableton/platforms/esp32/Clock.hpp new file mode 100644 index 0000000..5cfaf0b --- /dev/null +++ b/include/ableton/platforms/esp32/Clock.hpp @@ -0,0 +1,36 @@ +/* Copyright 2019, Mathias Bredholt, Torso Electronics, Copenhagen. All rights reserved. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "esp_timer.h" + +namespace ableton +{ +namespace platforms +{ +namespace esp32 +{ +struct Clock +{ + std::chrono::microseconds micros() const + { + return static_cast<std::chrono::microseconds>(esp_timer_get_time()); + } +}; +} // namespace esp32 +} // namespace platforms +} // namespace ableton diff --git a/include/ableton/platforms/esp32/Context.hpp b/include/ableton/platforms/esp32/Context.hpp new file mode 100644 index 0000000..772d38e --- /dev/null +++ b/include/ableton/platforms/esp32/Context.hpp @@ -0,0 +1,208 @@ +/* Copyright 2020, Ableton AG, Berlin. All rights reserved. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + * + * If you would like to incorporate Link into a proprietary software application, + * please contact <link-devs@ableton.com>. + */ + +#pragma once + +#include <ableton/discovery/IpV4Interface.hpp> +#include <ableton/platforms/asio/AsioTimer.hpp> +#include <ableton/platforms/asio/AsioWrapper.hpp> +#include <ableton/platforms/asio/Socket.hpp> +#include <ableton/platforms/esp32/LockFreeCallbackDispatcher.hpp> +#include <freertos/task.h> + +namespace ableton +{ +namespace platforms +{ +namespace esp32 +{ + +template <typename ScanIpIfAddrs, typename LogT> +class Context +{ + class ServiceRunner + { + + static void run(void* userParams) + { + auto runner = static_cast<ServiceRunner*>(userParams); + for (;;) + { + try + { + runner->mpService->poll_one(); + } + catch (...) + { + } + portYIELD(); + } + } + + public: + ServiceRunner() + : mpService(new ::asio::io_service()) + , mpWork(new ::asio::io_service::work(*mpService)) + { + xTaskCreatePinnedToCore( + run, "link", 8192, this, 2 | portPRIVILEGE_BIT, &mTaskHandle, LINK_ESP_TASK_CORE_ID); + } + + ~ServiceRunner() + { + vTaskDelete(mTaskHandle); + } + + template <typename Handler> + void async(Handler handler) + { + mpService->post(std::move(handler)); + } + + ::asio::io_service& service() const + { + return *mpService; + } + + private: + TaskHandle_t mTaskHandle; + std::unique_ptr<::asio::io_service> mpService; + std::unique_ptr<::asio::io_service::work> mpWork; + }; + +public: + using Timer = ::ableton::platforms::asio::AsioTimer; + using Log = LogT; + + template <typename Handler, typename Duration> + using LockFreeCallbackDispatcher = LockFreeCallbackDispatcher<Handler, Duration>; + + template <std::size_t BufferSize> + using Socket = asio::Socket<BufferSize>; + + Context() + : Context(DefaultHandler{}) + { + } + + template <typename ExceptionHandler> + explicit Context(ExceptionHandler exceptHandler) + { + } + + Context(const Context&) = delete; + + Context(Context&& rhs) + : mLog(std::move(rhs.mLog)) + , mScanIpIfAddrs(std::move(rhs.mScanIpIfAddrs)) + { + } + + void stop() + { + } + + template <std::size_t BufferSize> + Socket<BufferSize> openUnicastSocket(const ::asio::ip::address_v4& addr) + { + auto socket = Socket<BufferSize>{serviceRunner().service()}; + socket.mpImpl->mSocket.set_option( + ::asio::ip::multicast::enable_loopback(addr.is_loopback())); + socket.mpImpl->mSocket.set_option(::asio::ip::multicast::outbound_interface(addr)); + socket.mpImpl->mSocket.bind(::asio::ip::udp::endpoint{addr, 0}); + return socket; + } + + template <std::size_t BufferSize> + Socket<BufferSize> openMulticastSocket(const ::asio::ip::address_v4& addr) + { + auto socket = Socket<BufferSize>{serviceRunner().service()}; + socket.mpImpl->mSocket.set_option(::asio::ip::udp::socket::reuse_address(true)); + socket.mpImpl->mSocket.set_option( + ::asio::socket_base::broadcast(!addr.is_loopback())); + socket.mpImpl->mSocket.set_option( + ::asio::ip::multicast::enable_loopback(addr.is_loopback())); + socket.mpImpl->mSocket.set_option(::asio::ip::multicast::outbound_interface(addr)); + socket.mpImpl->mSocket.bind({::asio::ip::address::from_string("0.0.0.0"), + discovery::multicastEndpoint().port()}); + socket.mpImpl->mSocket.set_option(::asio::ip::multicast::join_group( + discovery::multicastEndpoint().address().to_v4(), addr)); + return socket; + } + + std::vector<::asio::ip::address> scanNetworkInterfaces() + { + return mScanIpIfAddrs(); + } + + Timer makeTimer() const + { + return {serviceRunner().service()}; + } + + Log& log() + { + return mLog; + } + + template <typename Handler> + void async(Handler handler) + { + serviceRunner().service().post(std::move(handler)); + } + + Context clone() const + { + return {}; + } + + template <typename ExceptionHandler> + Context clone(ExceptionHandler handler) const + { + return Context{std::move(handler)}; + } + +private: + // Default handler is hidden and defines a hidden exception type + // that will never be thrown by other code, so it effectively does + // not catch. + struct DefaultHandler + { + struct Exception + { + }; + + void operator()(const Exception&) + { + } + }; + + static ServiceRunner& serviceRunner() + { + static ServiceRunner runner; + return runner; + } + + Log mLog; + ScanIpIfAddrs mScanIpIfAddrs; +}; + +} // namespace esp32 +} // namespace platforms +} // namespace ableton diff --git a/include/ableton/platforms/esp32/Esp32.hpp b/include/ableton/platforms/esp32/Esp32.hpp new file mode 100644 index 0000000..9762ef4 --- /dev/null +++ b/include/ableton/platforms/esp32/Esp32.hpp @@ -0,0 +1,27 @@ +/* Copyright 2019, Mathias Bredholt, Torso Electronics, Copenhagen. All rights reserved. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <endian.h> + +#ifndef ntohll +#define ntohll(x) bswap64(x) +#endif + +#ifndef htonll +#define htonll(x) bswap64(x) +#endif diff --git a/include/ableton/platforms/esp32/LockFreeCallbackDispatcher.hpp b/include/ableton/platforms/esp32/LockFreeCallbackDispatcher.hpp new file mode 100644 index 0000000..b02ab73 --- /dev/null +++ b/include/ableton/platforms/esp32/LockFreeCallbackDispatcher.hpp @@ -0,0 +1,94 @@ +/* Copyright 2020, Ableton AG, Berlin. All rights reserved. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + * + * If you would like to incorporate Link into a proprietary software application, + * please contact <link-devs@ableton.com>. + */ + +#pragma once + +#include <atomic> +#include <condition_variable> +#include <freertos/task.h> + +namespace ableton +{ +namespace platforms +{ +namespace esp32 +{ + +// Utility to signal invocation of a callback on another thread in a lock free manner. +// The callback is evoked on a thread owned by the instance of this class. +// +// A condition variable is used to notify a waiting thread, but only if the required +// lock can be acquired immediately. If that fails, we fall back on signaling +// after a timeout. This gives us a guaranteed minimum signaling rate which is defined +// by the fallbackPeriod parameter. + +template <typename Callback, typename Duration> +class LockFreeCallbackDispatcher +{ +public: + LockFreeCallbackDispatcher(Callback callback, Duration fallbackPeriod) + : mCallback(std::move(callback)) + , mFallbackPeriod(std::move(fallbackPeriod)) + , mRunning(true) + { + xTaskCreate(run, "link", 4096, this, tskIDLE_PRIORITY, &mTaskHandle); + } + + ~LockFreeCallbackDispatcher() + { + mRunning = false; + mCondition.notify_one(); + vTaskDelete(mTaskHandle); + } + + void invoke() + { + if (mMutex.try_lock()) + { + mCondition.notify_one(); + mMutex.unlock(); + } + } + +private: + static void run(void* userData) + { + auto dispatcher = static_cast<LockFreeCallbackDispatcher*>(userData); + while (dispatcher->mRunning.load()) + { + { + std::unique_lock<std::mutex> lock(dispatcher->mMutex); + dispatcher->mCondition.wait_for(lock, dispatcher->mFallbackPeriod); + } + dispatcher->mCallback(); + portYIELD(); + } + } + + Callback mCallback; + Duration mFallbackPeriod; + std::atomic<bool> mRunning; + std::mutex mMutex; + std::condition_variable mCondition; + TaskHandle_t mTaskHandle; +}; + +} // namespace esp32 +} // namespace platforms +} // namespace ableton diff --git a/include/ableton/platforms/esp32/Random.hpp b/include/ableton/platforms/esp32/Random.hpp new file mode 100644 index 0000000..adca049 --- /dev/null +++ b/include/ableton/platforms/esp32/Random.hpp @@ -0,0 +1,36 @@ +/* Copyright 2019, Ableton AG, Berlin. All rights reserved. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +namespace ableton +{ +namespace platforms +{ +namespace esp32 +{ + +struct Random +{ + uint8_t operator()() + { + return static_cast<uint8_t>((esp_random() % 93) + 33); // printable ascii chars + } +}; + +} // namespace esp32 +} // namespace platforms +} // namespace ableton diff --git a/include/ableton/platforms/esp32/ScanIpIfAddrs.hpp b/include/ableton/platforms/esp32/ScanIpIfAddrs.hpp new file mode 100644 index 0000000..26406a1 --- /dev/null +++ b/include/ableton/platforms/esp32/ScanIpIfAddrs.hpp @@ -0,0 +1,51 @@ +/* Copyright 2020, Ableton AG, Berlin and 2019, Mathias Bredholt, Torso Electronics, + * Copenhagen. All rights reserved. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <ableton/platforms/asio/AsioWrapper.hpp> +#include <arpa/inet.h> +#include <net/if.h> +#include <tcpip_adapter.h> +#include <vector> + +namespace ableton +{ +namespace platforms +{ +namespace esp32 +{ + +// ESP32 implementation of ip interface address scanner +struct ScanIpIfAddrs +{ + std::vector<::asio::ip::address> operator()() + { + std::vector<::asio::ip::address> addrs; + tcpip_adapter_ip_info_t ip_info; + if (ESP_OK == tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info) + && tcpip_adapter_is_netif_up(TCPIP_ADAPTER_IF_STA) && ip_info.ip.addr) + { + addrs.emplace_back(::asio::ip::address_v4(ntohl(ip_info.ip.addr))); + } + return addrs; + } +}; + +} // namespace esp32 +} // namespace platforms +} // namespace ableton diff --git a/examples/qlinkhut/main.cpp b/include/ableton/platforms/stl/Random.hpp index bc6058c..e4b224b 100644 --- a/examples/qlinkhut/main.cpp +++ b/include/ableton/platforms/stl/Random.hpp @@ -1,4 +1,4 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. +/* Copyright 2019, Ableton AG, Berlin. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,20 +17,34 @@ * please contact <link-devs@ableton.com>. */ -#include "Controller.hpp" -#include <QGuiApplication> -#include <QQmlApplicationEngine> -#include <QQmlContext> -#include <QQuickView> -#include <QQuickWindow> +#pragma once -int main(int argc, char* argv[]) +#include <random> + +namespace ableton +{ +namespace platforms +{ +namespace stl +{ + +struct Random { - ableton::qlinkhut::Controller controller; - QGuiApplication app(argc, argv); - QQuickView view; - view.rootContext()->setContextProperty("controller", &controller); - view.setSource(QUrl("qrc:/main.qml")); - view.show(); - return app.exec(); -} + Random() + : gen(rd()) + , dist(33, 126) // printable ascii chars + {} + + uint8_t operator()() + { + return static_cast<uint8_t>(dist(gen)); + } + + std::random_device rd; + std::mt19937 gen; + std::uniform_int_distribution<unsigned> dist; +}; + +} // namespace stl +} // namespace platforms +} // namespace ableton diff --git a/include/ableton/platforms/windows/Clock.hpp b/include/ableton/platforms/windows/Clock.hpp index 87bfeab..b8f9029 100644 --- a/include/ableton/platforms/windows/Clock.hpp +++ b/include/ableton/platforms/windows/Clock.hpp @@ -38,17 +38,17 @@ struct Clock { LARGE_INTEGER frequency; QueryPerformanceFrequency(&frequency); - mTicksToMicros = 1.0e6 / frequency.QuadPart; + mTicksToMicros = 1.0e6 / static_cast<double>(frequency.QuadPart); } Micros ticksToMicros(const Ticks ticks) const { - return Micros{llround(mTicksToMicros * ticks)}; + return Micros{llround(mTicksToMicros * static_cast<double>(ticks))}; } Ticks microsToTicks(const Micros micros) const { - return static_cast<Ticks>(micros.count() / mTicksToMicros); + return static_cast<Ticks>(static_cast<double>(micros.count()) / mTicksToMicros); } Ticks ticks() const diff --git a/examples/qlinkhut/BeatTile.qml b/include/ableton/platforms/windows/Windows.hpp index 160a18f..0b835d1 100644 --- a/examples/qlinkhut/BeatTile.qml +++ b/include/ableton/platforms/windows/Windows.hpp @@ -1,4 +1,4 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. +/* Copyright 2020, Ableton AG, Berlin. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,16 +17,16 @@ * please contact <link-devs@ableton.com>. */ -import QtQuick 2.0 +#pragma once -Rectangle { - property var index: 0 - property var currentBeat: -1 - property var countIn: false - property var activeColor: "#404040" - property var countInColor: "#909090" - height: 328; - border.width: 1; - border.color: "#FFFFFF"; - color: index == currentBeat ? (countIn ? countInColor : activeColor) : "#404040" -} +// ntohll and htonll are not defined for MinGW + +#ifdef __MINGW32__ +#if __BIG_ENDIAN__ +#define htonll(x) (x) +#define ntohll(x) (x) +#else +#define htonll(x) (((uint64_t)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32)) +#define ntohll(x) (((uint64_t)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32)) +#endif +#endif diff --git a/include/ableton/test/serial_io/Context.hpp b/include/ableton/test/serial_io/Context.hpp index a2b1406..ecc738d 100644 --- a/include/ableton/test/serial_io/Context.hpp +++ b/include/ableton/test/serial_io/Context.hpp @@ -67,6 +67,10 @@ public: { } + void stop() + { + } + template <typename Handler> void async(Handler handler) { diff --git a/include/ableton/test/serial_io/SchedulerTree.hpp b/include/ableton/test/serial_io/SchedulerTree.hpp index e9f4766..6b85478 100644 --- a/include/ableton/test/serial_io/SchedulerTree.hpp +++ b/include/ableton/test/serial_io/SchedulerTree.hpp @@ -53,7 +53,7 @@ public: void setTimer(const TimerId timerId, const TimePoint expiration, Handler handler) { using namespace std; - mTimers[make_pair(move(expiration), timerId)] = move(handler); + mTimers[make_pair(std::move(expiration), timerId)] = std::move(handler); } void cancelTimer(const TimerId timerId); diff --git a/include/ableton/test/serial_io/Socket.hpp b/include/ableton/test/serial_io/Socket.hpp deleted file mode 100644 index 74b5c4c..0000000 --- a/include/ableton/test/serial_io/Socket.hpp +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright 2016, Ableton AG, Berlin. All rights reserved. - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - * - * If you would like to incorporate Link into a proprietary software application, - * please contact <link-devs@ableton.com>. - */ - -#pragma once - -#include <ableton/platforms/AsioWrapper.hpp> -#include <functional> - -namespace ableton -{ -namespace test -{ -namespace serial_io -{ - -struct Socket -{ - using SendFn = std::function<std::size_t( - const uint8_t* const, const size_t, const asio::ip::udp::endpoint&)>; - - struct ReceiveHandler - { - template <typename It> - void operator()(const asio::ip::udp::endpoint& from, const It begin, const It end) - { - std::vector<uint8_t> buffer{begin, end}; - mReceive(from, buffer); - } - - std::function<const asio::ip::udp::endpoint&, const std::vector<uint8_t>&> mReceive; - }; - - using ReceiveFn = std::function<void(ReceiveHandler)>; - - Socket(SendFn sendFn, ReceiveFn receiveFn) - : mSendFn(std::move(sendFn)) - , mReceiveFn(std::move(receiveFn)) - { - } - - std::size_t send( - const uint8_t* const pData, const size_t numBytes, const asio::ip::udp::endpoint& to) - { - return mSendFn(pData, numBytes, to); - } - - template <typename Handler> - void receive(Handler handler) - { - mReceiveFn(ReceiveHandler{ - [handler](const asio::ip::udp::endpoint& from, const std::vector<uint8_t>& buffer) { - handler(from, begin(buffer), end(buffer)); - }}); - } - - asio::ip::udp::endpoint endpoint() const - { - return asio::ip::udp::endpoint({}, 0); - } - - SendFn mSendFn; - ReceiveFn mReceiveFn; -}; - -} // namespace serial_io -} // namespace test -} // namespace ableton diff --git a/src/ableton/link/tst_Controller.cpp b/src/ableton/link/tst_Controller.cpp index d65403c..4439cf2 100644 --- a/src/ableton/link/tst_Controller.cpp +++ b/src/ableton/link/tst_Controller.cpp @@ -19,7 +19,9 @@ #include <ableton/link/Controller.hpp> #include <ableton/link/Tempo.hpp> +#include <ableton/platforms/stl/Random.hpp> #include <ableton/test/CatchWrapper.hpp> +#include <ableton/util/Log.hpp> #include <ableton/util/test/Timer.hpp> namespace ableton @@ -82,6 +84,10 @@ struct MockIoContext } }; + void stop() + { + } + template <std::size_t BufferSize> Socket<BufferSize> openUnicastSocket(const asio::ip::address_v4&) { @@ -151,6 +157,7 @@ using MockController = Controller<PeerCountCallback, TempoCallback, StartStopStateCallback, MockClock, + platforms::stl::Random, MockIoContext>; const auto kAnyBeatTime = Beats{5.}; @@ -191,7 +198,7 @@ void testSetAndGetClientState( const auto initialTimeline = Optional<Timeline>{Timeline{Tempo{60.}, Beats{0.}, kAnyTime}}; const auto initialStartStopState = - Optional<StartStopState>{StartStopState{true, kAnyBeatTime, clock.micros()}}; + Optional<ClientStartStopState>{ClientStartStopState{false, kAnyTime, clock.micros()}}; const auto initialClientState = IncomingClientState{initialTimeline, initialStartStopState, clock.micros()}; @@ -206,8 +213,8 @@ void testSetAndGetClientState( { // Set client state with a StartStopState having the same timestamp as the current // StartStopState - don't advance clock - const auto outdatedStartStopState = - Optional<StartStopState>{StartStopState{false, kAnyBeatTime, clock.micros()}}; + const auto outdatedStartStopState = Optional<ClientStartStopState>{ + ClientStartStopState{false, kAnyTime, clock.micros()}}; setClientState(controller, IncomingClientState{Optional<Timeline>{}, outdatedStartStopState, clock.micros()}); CHECK(initialClientState == getClientState(controller)); @@ -217,9 +224,8 @@ void testSetAndGetClientState( SECTION("Set outdated start stop state (timestamp in past)") { - const auto outdatedStartStopState = - Optional<StartStopState>{StartStopState{false, kAnyBeatTime, microseconds{0}}}; - + const auto outdatedStartStopState = Optional<ClientStartStopState>{ + ClientStartStopState{false, kAnyTime, microseconds{0}}}; setClientState(controller, IncomingClientState{Optional<Timeline>{}, outdatedStartStopState, clock.micros()}); CHECK(initialClientState == getClientState(controller)); @@ -228,7 +234,7 @@ void testSetAndGetClientState( SECTION("Set empty client state") { setClientState(controller, IncomingClientState{Optional<Timeline>{}, - Optional<StartStopState>{}, clock.micros()}); + Optional<ClientStartStopState>{}, clock.micros()}); CHECK(initialClientState == getClientState(controller)); } @@ -236,8 +242,8 @@ void testSetAndGetClientState( { const auto expectedTimeline = Optional<Timeline>{Timeline{Tempo{80.}, Beats{1.}, kAnyTime}}; - const auto expectedStartStopState = - Optional<StartStopState>{StartStopState{false, kAnyBeatTime, clock.micros()}}; + const auto expectedStartStopState = Optional<ClientStartStopState>{ + ClientStartStopState{false, kAnyTime, clock.micros()}}; const auto expectedClientState = IncomingClientState{expectedTimeline, expectedStartStopState, clock.micros()}; setClientState(controller, expectedClientState); @@ -264,8 +270,8 @@ void testCallbackInvocation(SetClientStateFunctionT setClientState) const auto initialTimeline = Optional<Timeline>{Timeline{initialTempo, Beats{0.}, kAnyTime}}; - const auto initialStartStopState = Optional<StartStopState>{ - StartStopState{initialIsPlaying, kAnyBeatTime, clock.micros()}}; + const auto initialStartStopState = Optional<ClientStartStopState>{ + ClientStartStopState{initialIsPlaying, kAnyTime, clock.micros()}}; setClientState(controller, {initialTimeline, initialStartStopState, clock.micros()}); SECTION("Callbacks are called when setting new client state") @@ -281,8 +287,8 @@ void testCallbackInvocation(SetClientStateFunctionT setClientState) { const auto timeline = Optional<Timeline>{Timeline{initialTempo, Beats{1.}, kAnyTime}}; - const auto startStopState = Optional<StartStopState>{ - StartStopState{initialIsPlaying, kAnyBeatTime, clock.micros()}}; + const auto startStopState = Optional<ClientStartStopState>{ + ClientStartStopState{initialIsPlaying, kAnyTime, clock.micros()}}; setClientState(controller, {timeline, startStopState, clock.micros()}); CHECK(tempoCallback.tempos.empty()); CHECK(startStopStateCallback.startStopStates.empty()); @@ -408,7 +414,7 @@ TEST_CASE("Controller | GetClientStateRtSafeGracePeriod", "[Controller]") const auto initialTimeline = Optional<Timeline>{Timeline{Tempo{50.}, Beats{0.}, clock.micros()}}; const auto initialStartStopState = - Optional<StartStopState>{StartStopState{true, kAnyBeatTime, clock.micros()}}; + Optional<ClientStartStopState>{ClientStartStopState{true, kAnyTime, clock.micros()}}; const auto initialState = IncomingClientState{initialTimeline, initialStartStopState, clock.micros()}; @@ -421,7 +427,7 @@ TEST_CASE("Controller | GetClientStateRtSafeGracePeriod", "[Controller]") const auto newTimeline = Optional<Timeline>{Timeline{Tempo{70.}, Beats{1.}, clock.micros()}}; const auto newStartStopState = - Optional<StartStopState>{StartStopState{false, kAnyBeatTime, clock.micros()}}; + Optional<ClientStartStopState>{ClientStartStopState{false, kAnyTime, clock.micros()}}; const auto newState = IncomingClientState{newTimeline, newStartStopState, clock.micros()}; diff --git a/src/ableton/link/tst_Measurement.cpp b/src/ableton/link/tst_Measurement.cpp index 9fea333..89edf1d 100644 --- a/src/ableton/link/tst_Measurement.cpp +++ b/src/ableton/link/tst_Measurement.cpp @@ -19,13 +19,12 @@ #include <ableton/discovery/Payload.hpp> #include <ableton/discovery/test/Socket.hpp> -#include <ableton/link/GhostXForm.hpp> #include <ableton/link/Measurement.hpp> #include <ableton/link/SessionId.hpp> #include <ableton/link/v1/Messages.hpp> -#include <ableton/platforms/asio/AsioWrapper.hpp> +#include <ableton/platforms/stl/Random.hpp> #include <ableton/test/CatchWrapper.hpp> -#include <ableton/util/Log.hpp> +#include <ableton/util/test/IoService.hpp> #include <array> namespace ableton @@ -35,8 +34,6 @@ namespace link namespace { -using Socket = discovery::test::Socket; - struct MockClock { std::chrono::microseconds micros() const @@ -45,28 +42,56 @@ struct MockClock } }; +struct MockIoContext +{ + template <std::size_t BufferSize> + using Socket = discovery::test::Socket; + + template <std::size_t BufferSize> + Socket<BufferSize> openUnicastSocket(const asio::ip::address_v4&) + { + return Socket<BufferSize>(mIo); + } + + using Timer = util::test::Timer; + + Timer makeTimer() + { + return {}; + } + + using Log = util::NullLog; + + Log log() const + { + return {}; + } + + ableton::util::test::IoService mIo; +}; + struct TFixture { TFixture() - : mAddress(asio::ip::address_v4::from_string("127.0.0.1")) - , mMeasurement(mStateQuery(), + : mMeasurement(mStateQuery(), [](std::vector<std::pair<double, double>>) {}, - mAddress, + {}, MockClock{}, - util::injectVal(util::NullLog{})) + MockIoContext{}) { } - Socket& socket() + discovery::test::Socket socket() { - return *(mMeasurement.mpImpl->mpSocket); + return mMeasurement.mpImpl->mSocket; } struct StateQuery { StateQuery() { - mState.nodeState.sessionId = NodeId::random(); + using Random = ableton::platforms::stl::Random; + mState.nodeState.sessionId = NodeId::random<Random>(); mState.endpoint = asio::ip::udp::endpoint(asio::ip::address_v4::from_string("127.0.0.1"), 9999); } @@ -78,24 +103,13 @@ struct TFixture PeerState mState; }; - struct GhostXFormQuery - { - GhostXForm operator()() const - { - return {1.0, std::chrono::microseconds{0}}; - } - }; - StateQuery mStateQuery; - GhostXFormQuery mGhostXFormQuery; - asio::ip::address_v4 mAddress; - Measurement<util::test::IoService, MockClock, discovery::test::Socket, util::NullLog> - mMeasurement; + Measurement<MockClock, MockIoContext> mMeasurement; }; } // anonymous namespace -TEST_CASE("Measurement | SendPingsOnConstruction", "[Measurement]") +TEST_CASE("PeerMeasurement | SendPingsOnConstruction", "[PeerMeasurement]") { TFixture fixture; CHECK(1 == fixture.socket().sentMessages.size()); @@ -116,7 +130,7 @@ TEST_CASE("Measurement | SendPingsOnConstruction", "[Measurement]") CHECK(std::chrono::microseconds{0} == gt); } -TEST_CASE("Measurement | ReceiveInitPong", "[Measurement]") +TEST_CASE("PeerMeasurement | ReceiveInitPong", "[PeerMeasurement]") { using Micros = std::chrono::microseconds; TFixture fixture; @@ -138,7 +152,7 @@ TEST_CASE("Measurement | ReceiveInitPong", "[Measurement]") CHECK(0 == fixture.mMeasurement.mpImpl->mData.size()); } -TEST_CASE("Measurement | ReceivePong", "[Measurement]") +TEST_CASE("PeerMeasurement | ReceivePong", "[PeerMeasurement]") { using Micros = std::chrono::microseconds; TFixture fixture; diff --git a/src/ableton/link/tst_Peers.cpp b/src/ableton/link/tst_Peers.cpp index b1ebb95..a8060f5 100644 --- a/src/ableton/link/tst_Peers.cpp +++ b/src/ableton/link/tst_Peers.cpp @@ -18,6 +18,7 @@ */ #include <ableton/link/Peers.hpp> +#include <ableton/platforms/stl/Random.hpp> #include <ableton/test/CatchWrapper.hpp> #include <ableton/test/serial_io/Fixture.hpp> @@ -28,6 +29,8 @@ namespace link namespace { +using Random = ableton::platforms::stl::Random; + struct SessionMembershipCallback { void operator()() @@ -59,18 +62,18 @@ struct SessionStartStopStateCallback }; const auto fooPeer = - PeerState{{NodeId::random(), NodeId::random(), + PeerState{{NodeId::random<Random>(), NodeId::random<Random>(), Timeline{Tempo{60.}, Beats{1.}, std::chrono::microseconds{1234}}, StartStopState{false, Beats{0.}, std::chrono::microseconds{2345}}}, {}}; const auto barPeer = - PeerState{{NodeId::random(), NodeId::random(), + PeerState{{NodeId::random<Random>(), NodeId::random<Random>(), Timeline{Tempo{120.}, Beats{10.}, std::chrono::microseconds{500}}, {}}, {}}; const auto bazPeer = - PeerState{{NodeId::random(), NodeId::random(), + PeerState{{NodeId::random<Random>(), NodeId::random<Random>(), Timeline{Tempo{100.}, Beats{4.}, std::chrono::microseconds{100}}, {}}, {}}; @@ -113,10 +116,10 @@ TEST_CASE("Peers | EmptySessionPeersAfterInit", "[Peers]") TEST_CASE("Peers | AddAndFindPeer", "[Peers]") { - test::serial_io::Fixture io; auto membership = SessionMembershipCallback{}; auto sessions = SessionTimelineCallback{}; auto startStops = SessionStartStopStateCallback{}; + test::serial_io::Fixture io; auto peers = makePeers(util::injectVal(io.makeIoContext()), std::ref(membership), std::ref(sessions), std::ref(startStops)); auto observer = makeGatewayObserver(peers, gateway1); @@ -133,8 +136,8 @@ TEST_CASE("Peers | AddAndFindPeer", "[Peers]") TEST_CASE("Peers | AddAndRemovePeer", "[Peers]") { - test::serial_io::Fixture io; auto membership = SessionMembershipCallback{}; + test::serial_io::Fixture io; auto peers = makePeers(util::injectVal(io.makeIoContext()), std::ref(membership), SessionTimelineCallback{}, SessionStartStopStateCallback{}); auto observer = makeGatewayObserver(peers, gateway1); @@ -149,10 +152,10 @@ TEST_CASE("Peers | AddAndRemovePeer", "[Peers]") TEST_CASE("Peers | AddTwoPeersRemoveOne", "[Peers]") { - test::serial_io::Fixture io; auto membership = SessionMembershipCallback{}; auto sessions = SessionTimelineCallback{}; auto startStops = SessionStartStopStateCallback{}; + test::serial_io::Fixture io; auto peers = makePeers(util::injectVal(io.makeIoContext()), std::ref(membership), std::ref(sessions), std::ref(startStops)); auto observer = makeGatewayObserver(peers, gateway1); @@ -169,10 +172,10 @@ TEST_CASE("Peers | AddTwoPeersRemoveOne", "[Peers]") TEST_CASE("Peers | AddThreePeersTwoOnSameGateway", "[Peers]") { - test::serial_io::Fixture io; auto membership = SessionMembershipCallback{}; auto sessions = SessionTimelineCallback{}; auto startStops = SessionStartStopStateCallback{}; + test::serial_io::Fixture io; auto peers = makePeers(util::injectVal(io.makeIoContext()), std::ref(membership), std::ref(sessions), std::ref(startStops)); auto observer1 = makeGatewayObserver(peers, gateway1); @@ -191,10 +194,10 @@ TEST_CASE("Peers | AddThreePeersTwoOnSameGateway", "[Peers]") TEST_CASE("Peers | CloseGateway", "[Peers]") { - test::serial_io::Fixture io; auto membership = SessionMembershipCallback{}; auto sessions = SessionTimelineCallback{}; auto startStops = SessionStartStopStateCallback{}; + test::serial_io::Fixture io; auto peers = makePeers(util::injectVal(io.makeIoContext()), std::ref(membership), std::ref(sessions), std::ref(startStops)); auto observer1 = makeGatewayObserver(peers, gateway1); diff --git a/src/ableton/link/tst_PingResponder.cpp b/src/ableton/link/tst_PingResponder.cpp index f6a7568..8f0c095 100644 --- a/src/ableton/link/tst_PingResponder.cpp +++ b/src/ableton/link/tst_PingResponder.cpp @@ -23,11 +23,9 @@ #include <ableton/link/NodeState.hpp> #include <ableton/link/PayloadEntries.hpp> #include <ableton/link/PingResponder.hpp> -#include <ableton/link/SessionId.hpp> #include <ableton/link/v1/Messages.hpp> -#include <ableton/platforms/asio/AsioWrapper.hpp> +#include <ableton/platforms/stl/Random.hpp> #include <ableton/test/CatchWrapper.hpp> -#include <ableton/util/Log.hpp> #include <ableton/util/test/IoService.hpp> #include <array> @@ -38,16 +36,52 @@ namespace link namespace { +using Random = ableton::platforms::stl::Random; + +struct MockClock +{ + std::chrono::microseconds micros() const + { + return std::chrono::microseconds{4}; + } +}; + +struct MockIoContext +{ + template <std::size_t BufferSize> + using Socket = discovery::test::Socket; + + template <std::size_t BufferSize> + Socket<BufferSize> openUnicastSocket(const asio::ip::address_v4&) + { + return Socket<BufferSize>(mIo); + } + + using Log = util::NullLog; + + Log log() const + { + return {}; + } + + template <typename Handler> + void async(Handler handler) const + { + handler(); + } + + ableton::util::test::IoService mIo; +}; + struct RpFixture { RpFixture() : mAddress(asio::ip::address_v4::from_string("127.0.0.1")) , mResponder(mAddress, - NodeId::random(), + NodeId::random<Random>(), GhostXForm{1.0, std::chrono::microseconds{0}}, - util::injectVal(util::test::IoService{}), MockClock{}, - util::injectVal(util::NullLog{})) + util::injectRef(*mIo)) { } @@ -61,17 +95,9 @@ struct RpFixture return responderSocket().sentMessages.size(); } - struct MockClock - { - std::chrono::microseconds micros() const - { - return std::chrono::microseconds{4}; - } - }; - asio::ip::address_v4 mAddress = asio::ip::address_v4::from_string("127.0.0.1"); - PingResponder<util::test::IoService, MockClock, discovery::test::Socket, util::NullLog> - mResponder; + util::Injected<MockIoContext> mIo; + PingResponder<MockClock, MockIoContext> mResponder; }; } // anonymous namespace |