Skip to content

Commit

Permalink
Replaced Abletons HostTimeFilter by an own single header implementati…
Browse files Browse the repository at this point in the history
…on, to keep the dependency to Ableton Link out of the sounddevices code
  • Loading branch information
JoergAtGithub committed Jan 4, 2025
1 parent 29f7a1f commit 0a6694b
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,7 @@ set(
src/util/font.h
src/util/fpclassify.h
src/util/gitinfostore.h
src/util/hosttimefilter.h
src/util/imagefiledata.h
src/util/imageutils.h
src/util/indexrange.h
Expand Down Expand Up @@ -2442,6 +2443,7 @@ add_executable(
src/test/fileinfo_test.cpp
src/test/frametest.cpp
src/test/globaltrackcache_test.cpp
src/test/hosttimefilter_test.cpp
src/test/hotcuecontrol_test.cpp
src/test/imageutils_test.cpp
src/test/indexrange_test.cpp
Expand Down
19 changes: 15 additions & 4 deletions src/soundio/sounddevicenetwork.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
#include "util/trace.h"
#include "waveform/visualplayposition.h"

// HostTime clock reference type
// note that the resolution of std::chrono::steady_clock is not guaranteed
// to be high resolution, but it is guaranteed to be monotonic.
// However, on all major platforms, it is high resolution enough.
using ClockT = std::chrono::steady_clock;

namespace {
constexpr int kNetworkLatencyFrames = 8192; // 185 ms @ 44100 Hz
// Related chunk sizes:
Expand All @@ -41,7 +47,8 @@ SoundDeviceNetwork::SoundDeviceNetwork(
m_audioLatencyUsage(kAppGroup, QStringLiteral("audio_latency_usage")),
m_framesSinceAudioLatencyUsageUpdate(0),
m_denormals(false),
m_targetTime(0) {
m_targetTime(0),
m_hostTimeFilter(512) {
// Setting parent class members:
m_hostAPI = "Network stream";
m_sampleRate = SoundManagerConfig::kMixxxDefaultSampleRate;
Expand Down Expand Up @@ -520,10 +527,14 @@ void SoundDeviceNetwork::updateCallbackEntryToDacTime(SINT framesPerBuffer) {
double callbackEntrytoDacSecs = (m_targetTime - currentTime) / 1000000.0;
callbackEntrytoDacSecs = math_max(callbackEntrytoDacSecs, 0.0001);

// Use Ableton's HostTimeFilter class to create a smooth linear regression
// Use HostTimeFilter class to create a smooth linear regression
// between absolute network time and absolute host time
m_absTimeWhenPrevOutputBufferReachesDac = m_hostTimeFilter.sampleTimeToHostTime(
static_cast<double>(currentTime)) +

auto hostTime = std::chrono::duration_cast<std::chrono::microseconds>(
ClockT::now().time_since_epoch());

m_absTimeWhenPrevOutputBufferReachesDac = m_hostTimeFilter.calcFilteredHostTime(
static_cast<double>(currentTime), hostTime) +
std::chrono::microseconds(static_cast<long long>(callbackEntrytoDacSecs * 1000000));

VisualPlayPosition::setCallbackEntryToDacSecs(callbackEntrytoDacSecs, m_clkRefTimer);
Expand Down
12 changes: 2 additions & 10 deletions src/soundio/sounddevicenetwork.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
#include <QSharedPointer>
#include <QString>
#include <QThread>
#include <ableton/link/HostTimeFilter.hpp>
#include <ableton/platforms/stl/Clock.hpp>

#ifdef __LINUX__
#include <pthread.h>
Expand All @@ -16,6 +14,7 @@
#include "engine/sidechain/networkoutputstreamworker.h"
#include "soundio/sounddevice.h"
#include "util/fifo.h"
#include "util/hosttimefilter.h"
#include "util/performancetimer.h"

#define CPU_USAGE_UPDATE_RATE 30 // in 1/s, fits to display frame rate
Expand All @@ -25,13 +24,6 @@ class SoundManager;
class EngineNetworkStream;
class SoundDeviceNetworkThread;

// std::chrono::steady_clock
// -> selected by keyword 'stl' in ableton-link
// Note that the resolution of std::chrono::steady_clock is not guaranteed
// to be high resolution, but it is guaranteed to be monotonic.
// However, on all major platforms, it is high resolution enough.
using MixxxClockRef = ableton::platforms::stl::Clock;

class SoundDeviceNetwork : public SoundDevice {
public:
SoundDeviceNetwork(UserSettingsPointer config,
Expand Down Expand Up @@ -78,7 +70,7 @@ class SoundDeviceNetwork : public SoundDevice {
qint64 m_targetTime;
PerformanceTimer m_clkRefTimer;

ableton::link::HostTimeFilter<MixxxClockRef> m_hostTimeFilter;
mixxx::HostTimeFilter m_hostTimeFilter;
};

class SoundDeviceNetworkThread : public QThread {
Expand Down
17 changes: 13 additions & 4 deletions src/soundio/sounddeviceportaudio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
#include "util/trace.h"
#include "waveform/visualplayposition.h"

// HostTime clock reference type
// note that the resolution of std::chrono::steady_clock is not guaranteed
// to be high resolution, but it is guaranteed to be monotonic.
// However, on all major platforms, it is high resolution enough.
using ClockT = std::chrono::steady_clock;

#ifdef PA_USE_ALSA
// for PaAlsa_EnableRealtimeScheduling
#include <pa_linux_alsa.h>
Expand Down Expand Up @@ -99,8 +105,9 @@ SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config,
m_invalidTimeInfoCount(0),
m_lastCallbackEntrytoDacSecs(0),
m_callbackResult(paAbort),
m_hostTimeFilter(512),
m_cummulatedBufferTime(0),
m_meanOutputLatency(MovingInterquartileMean(501)) {
m_meanOutputLatency(MovingInterquartileMean(512)) {
// Setting parent class members:
m_hostAPI = Pa_GetHostApiInfo(deviceInfo->hostApi)->name;
m_sampleRate = mixxx::audio::SampleRate::fromDouble(deviceInfo->defaultSampleRate);
Expand Down Expand Up @@ -1090,14 +1097,16 @@ void SoundDevicePortAudio::updateCallbackEntryToDacTime(
- timeInfo->currentTime;
double bufferSizeSec = framesPerBuffer / m_sampleRate.toDouble();

// Use Ableton's HostTimeFilter class to create a smooth linear regression
// Use HostTimeFilter class to create a smooth linear regression
// between absolute sound card time and absolute host time
PaTime soundCardTimeNow = Pa_GetStreamTime(
m_pStream); // There is a delay & jitter to timeInfo->currentTime

m_cummulatedBufferTime += bufferSizeSec;
auto filteredHostTimeNow =
m_hostTimeFilter.sampleTimeToHostTime(m_cummulatedBufferTime);
auto hostTime = std::chrono::duration_cast<std::chrono::microseconds>(
ClockT::now().time_since_epoch());
auto filteredHostTimeNow = m_hostTimeFilter.calcFilteredHostTime(
m_cummulatedBufferTime, hostTime);

qWarning() << "Pa_GetStreamTime: "
<< static_cast<long long>(soundCardTimeNow * 1000000)
Expand Down
12 changes: 2 additions & 10 deletions src/soundio/sounddeviceportaudio.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,19 @@
#include <portaudio.h>

#include <QString>
#include <ableton/link/HostTimeFilter.hpp>
#include <ableton/platforms/stl/Clock.hpp>
#include <memory>

#include "control/pollingcontrolproxy.h"
#include "soundio/sounddevice.h"
#include "soundio/soundmanagerconfig.h"
#include "util/duration.h"
#include "util/fifo.h"
#include "util/hosttimefilter.h"
#include "util/movinginterquartilemean.h"
#include "util/performancetimer.h"

class SoundManager;

// std::chrono::steady_clock
// -> selected by keyword 'stl' in ableton-link
// Note that the resolution of std::chrono::steady_clock is not guaranteed
// to be high resolution, but it is guaranteed to be monotonic.
// However, on all major platforms, it is high resolution enough.
using MixxxClockRef = ableton::platforms::stl::Clock;

class SoundDevicePortAudio : public SoundDevice {
public:
SoundDevicePortAudio(UserSettingsPointer config,
Expand Down Expand Up @@ -96,7 +88,7 @@ class SoundDevicePortAudio : public SoundDevice {
PaTime m_lastCallbackEntrytoDacSecs;
std::atomic<int> m_callbackResult;

ableton::link::HostTimeFilter<MixxxClockRef> m_hostTimeFilter;
mixxx::HostTimeFilter m_hostTimeFilter;
double m_cummulatedBufferTime;
MovingInterquartileMean m_meanOutputLatency;
};
85 changes: 85 additions & 0 deletions src/test/hosttimefilter_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "util/hosttimefilter.h"

#include <gtest/gtest.h>

#include <chrono>

using namespace std::chrono_literals;

namespace mixxx {

class HostTimeFilterTest : public ::testing::Test {
protected:
HostTimeFilterTest()
: m_filter(5) { // Initialize with 5 points for testing
}

HostTimeFilter m_filter;
};

TEST_F(HostTimeFilterTest, InitialState) {
EXPECT_EQ(m_filter.calcFilteredHostTime(0.0, 0us), 0us);
}

TEST_F(HostTimeFilterTest, AddSinglePoint) {
EXPECT_NEAR(m_filter.calcFilteredHostTime(1.0, 1050us).count(), 1050, 1);
}

TEST_F(HostTimeFilterTest, EqualFreqNoJitter) {
// Wo perfectly synced clocks, the filter should return the same host time as the auxiliary time
EXPECT_NEAR(m_filter.calcFilteredHostTime(1000.0, 1000us).count(), 1000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(2000.0, 2000us).count(), 2000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(3000.0, 3000us).count(), 3000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(4000.0, 4000us).count(), 4000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(5000.0, 5000us).count(), 5000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(6000.0, 6000us).count(), 6000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(7000.0, 7000us).count(), 7000, 1);
}

TEST_F(HostTimeFilterTest, FasterFreqNoJitter) {
// Use 1024 sample buffer interval, instead of auxiliarry clock in time units
EXPECT_NEAR(m_filter.calcFilteredHostTime(1024.0, 1000us).count(), 1000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(2048.0, 2000us).count(), 2000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(3072.0, 3000us).count(), 3000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(4096.0, 4000us).count(), 4000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(5120.0, 5000us).count(), 5000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(6144.0, 6000us).count(), 6000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(7168.0, 7000us).count(), 7000, 1);
}

TEST_F(HostTimeFilterTest, FasterFreqWithJitter) {
// Use 1024 sample buffer interval, with 100us host time jitter
EXPECT_NEAR(m_filter.calcFilteredHostTime(1024.0, 1000us).count(), 1000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(2048.0, 2100us).count(), 2100, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(3072.0, 3000us).count(), 3033, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(4096.0, 3900us).count(), 3940, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(5120.0, 5000us).count(), 4960, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(6144.0, 6000us).count(), 5960, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(7168.0, 7000us).count(), 7000, 1);
}

TEST_F(HostTimeFilterTest, FasterFreqSkippedPoints) {
// Use 1024 sample buffer interval, instead of auxiliarry clock in time units
EXPECT_NEAR(m_filter.calcFilteredHostTime(1024.0, 1000us).count(), 1000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(2048.0, 2000us).count(), 2000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(4096.0, 4000us).count(), 4000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(5120.0, 5000us).count(), 5000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(8192.0, 8000us).count(), 8000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(9216.0, 9000us).count(), 9000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(11264.0, 11000us).count(), 11000, 1);
}

TEST_F(HostTimeFilterTest, Reset) {
m_filter.calcFilteredHostTime(1.0, 1050us);
m_filter.calcFilteredHostTime(2.0, 1950us);
m_filter.reset();
EXPECT_NEAR(m_filter.calcFilteredHostTime(4.0, 7777us).count(), 7777, 1);
}

TEST_F(HostTimeFilterTest, DenominatorZero) {
// Add two identical points to ensure the denominator becomes zero
EXPECT_NEAR(m_filter.calcFilteredHostTime(1.0, 1000us).count(), 1000, 1);
EXPECT_NEAR(m_filter.calcFilteredHostTime(1.0, 1000us).count(), 1000, 1);
}

} // namespace mixxx
84 changes: 84 additions & 0 deletions src/util/hosttimefilter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#pragma once

#include <chrono>
#include <utility>
#include <vector>

namespace mixxx {

class HostTimeFilter {
public:
explicit HostTimeFilter(const std::size_t numPoints)
: m_numPoints(numPoints),
m_index(0),
m_sumAux(0.0),
m_sumHst(0.0),
m_sumAuxByHst(0.0),
m_sumAuxSquared(0.0) {
m_points.reserve(m_numPoints);
}

void reset() {
m_index = 0;
m_points.clear();
m_sumAux = 0.0;
m_sumHst = 0.0;
m_sumAuxByHst = 0.0;
m_sumAuxSquared = 0.0;
}

std::chrono::microseconds calcFilteredHostTime(
double auxiliaryTime, std::chrono::microseconds hostTime) {
const auto micros = hostTime.count();
const auto timePoint = std::make_pair(auxiliaryTime, static_cast<double>(micros));

if (m_points.size() < m_numPoints) {
m_points.push_back(timePoint);
m_sumAux += timePoint.first;
m_sumHst += timePoint.second;
m_sumAuxByHst += timePoint.first * timePoint.second;
m_sumAuxSquared += timePoint.first * timePoint.first;
} else {
const auto& prevPoint = m_points[m_index];
m_sumAux += timePoint.first - prevPoint.first;
m_sumHst += timePoint.second - prevPoint.second;
m_sumAuxByHst += timePoint.first * timePoint.second -
prevPoint.first * prevPoint.second;
m_sumAuxSquared += timePoint.first * timePoint.first -
prevPoint.first * prevPoint.first;
m_points[m_index] = timePoint;
}
m_index = (m_index + 1) % m_numPoints;

return linearRegression(timePoint);
}

private:
const std::size_t m_numPoints;
std::size_t m_index;
std::vector<std::pair<double, double>> m_points;
double m_sumAux;
double m_sumHst;
double m_sumAuxByHst;
double m_sumAuxSquared;

std::chrono::microseconds linearRegression(const std::pair<double, double>& timePoint) const {
if (m_points.size() < 2) {
return std::chrono::microseconds(static_cast<long long>(timePoint.second));
}

const double n = static_cast<double>(m_points.size());
const double denominator = (n * m_sumAuxSquared - m_sumAux * m_sumAux);
if (denominator == 0.0) {
return std::chrono::microseconds(static_cast<long long>(timePoint.second));
}

const double slope = (n * m_sumAuxByHst - m_sumAux * m_sumHst) / denominator;
const double intercept = (m_sumHst - slope * m_sumAux) / n;

return std::chrono::microseconds(
static_cast<long long>(slope * timePoint.first + intercept));
}
};

} // namespace mixxx

0 comments on commit 0a6694b

Please sign in to comment.