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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ project(elementary VERSION 0.11.5)
option(ONLY_BUILD_WASM "Only build the wasm subdirectory" OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)

include(CTest)

if(${ONLY_BUILD_WASM})
add_subdirectory(runtime)
add_subdirectory(wasm)
Expand Down
11 changes: 11 additions & 0 deletions runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ target_include_directories(${TargetName} INTERFACE
${CMAKE_CURRENT_SOURCE_DIR})

add_library(elem::${TargetName} ALIAS ${TargetName})

if(BUILD_TESTING AND NOT ONLY_BUILD_WASM)
add_executable(runtime_tests tests/PhasorNodeTest.cpp)
target_link_libraries(runtime_tests PRIVATE elem::${TargetName})
target_compile_features(runtime_tests PRIVATE cxx_std_17)
add_test(NAME runtime_tests COMMAND runtime_tests)

add_executable(runtime_benchmarks tests/PhasorNodeBenchmark.cpp)
target_link_libraries(runtime_benchmarks PRIVATE elem::${TargetName})
target_compile_features(runtime_benchmarks PRIVATE cxx_std_17)
endif()
8 changes: 6 additions & 2 deletions runtime/elem/builtins/Core.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ namespace elem

template <typename FloatType, bool WithReset = false>
struct PhasorNode : public GraphNode<FloatType> {
using GraphNode<FloatType>::GraphNode;
PhasorNode(NodeId id, double sr, size_t blockSize)
: GraphNode<FloatType>(id, sr, blockSize)
, sampleInterval(FloatType(1.0) / FloatType(sr))
{}

FloatType tick (FloatType freq) {
FloatType step = freq * (FloatType(1.0) / FloatType(GraphNode<FloatType>::getSampleRate()));
FloatType step = freq * sampleInterval;
FloatType y = phase;

FloatType next = phase + step;
Expand Down Expand Up @@ -132,6 +135,7 @@ namespace elem
}

Change<FloatType> change;
FloatType sampleInterval;
FloatType phase = 0;
};

Expand Down
122 changes: 122 additions & 0 deletions runtime/tests/PhasorNodeBenchmark.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include "elem/builtins/Core.h"

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

namespace {

volatile double benchmarkSink = 0.0;

struct BenchmarkResult {
std::string name;
double medianMicroseconds;
double samplesPerSecond;
double checksum;
};

template <typename FloatType>
elem::BlockContext<FloatType> makeContext(FloatType const** inputs, size_t inputChannels, FloatType** outputs, size_t numSamples)
{
return elem::BlockContext<FloatType>{inputs, inputChannels, outputs, 1, numSamples, nullptr, true};
}

template <typename Fn>
BenchmarkResult runBenchmark(std::string const& name, size_t samplesPerBlock, size_t blocksPerRun, size_t runs, Fn&& fn)
{
std::vector<double> elapsed;
elapsed.reserve(runs);

double checksum = 0.0;

for (size_t run = 0; run < runs; ++run) {
auto const t0 = std::chrono::steady_clock::now();
checksum += fn();
auto const t1 = std::chrono::steady_clock::now();

elapsed.push_back(std::chrono::duration<double, std::micro>(t1 - t0).count());
}

std::sort(elapsed.begin(), elapsed.end());
auto const median = elapsed[elapsed.size() / 2];
auto const samples = static_cast<double>(samplesPerBlock * blocksPerRun);
benchmarkSink = checksum;

return BenchmarkResult{name, median, samples / (median / 1'000'000.0), checksum};
}

BenchmarkResult benchmarkPhasor(size_t samplesPerBlock, size_t blocksPerRun, size_t runs)
{
std::vector<double> rate(samplesPerBlock, 440.0);
std::vector<double> output(samplesPerBlock, 0.0);
double const* inputs[] = {rate.data()};
double* outputs[] = {output.data()};

elem::PhasorNode<double, false> phasor(1, 44'100.0, samplesPerBlock);
auto ctx = makeContext(inputs, 1, outputs, samplesPerBlock);

return runBenchmark("phasor", samplesPerBlock, blocksPerRun, runs, [&]() {
double checksum = 0.0;

for (size_t block = 0; block < blocksPerRun; ++block) {
phasor.process(ctx);
checksum += output[block % samplesPerBlock];
}

return checksum;
});
}

BenchmarkResult benchmarkSyncPhasor(size_t samplesPerBlock, size_t blocksPerRun, size_t runs)
{
std::vector<double> rate(samplesPerBlock, 440.0);
std::vector<double> reset(samplesPerBlock, 0.0);
std::vector<double> output(samplesPerBlock, 0.0);
double const* inputs[] = {rate.data(), reset.data()};
double* outputs[] = {output.data()};

elem::PhasorNode<double, true> phasor(2, 44'100.0, samplesPerBlock);
auto ctx = makeContext(inputs, 2, outputs, samplesPerBlock);

return runBenchmark("sphasor", samplesPerBlock, blocksPerRun, runs, [&]() {
double checksum = 0.0;

for (size_t block = 0; block < blocksPerRun; ++block) {
phasor.process(ctx);
checksum += output[block % samplesPerBlock];
}

return checksum;
});
}

void printResult(BenchmarkResult const& result)
{
std::cout << result.name
<< ",median_us=" << result.medianMicroseconds
<< ",samples_per_second=" << result.samplesPerSecond
<< ",checksum=" << result.checksum
<< '\n';
}

} // namespace

int main(int argc, char** argv)
{
auto const samplesPerBlock = argc > 1 ? static_cast<size_t>(std::stoull(argv[1])) : size_t{512};
auto const blocksPerRun = argc > 2 ? static_cast<size_t>(std::stoull(argv[2])) : size_t{1'000'000};
auto const runs = argc > 3 ? static_cast<size_t>(std::stoull(argv[3])) : size_t{7};

std::cout << "samples_per_block=" << samplesPerBlock
<< ",blocks_per_run=" << blocksPerRun
<< ",runs=" << runs << '\n';

printResult(benchmarkPhasor(samplesPerBlock, blocksPerRun, runs));
printResult(benchmarkSyncPhasor(samplesPerBlock, blocksPerRun, runs));

return benchmarkSink == 0.123456789 ? 1 : 0;
}
70 changes: 70 additions & 0 deletions runtime/tests/PhasorNodeTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include "elem/builtins/Core.h"

#include <cmath>
#include <iostream>

namespace {

template <typename FloatType>
elem::BlockContext<FloatType> makeContext(FloatType const** inputs, size_t inputChannels, FloatType** outputs, size_t numSamples)
{
return elem::BlockContext<FloatType>{inputs, inputChannels, outputs, 1, numSamples, nullptr, true};
}

bool closeEnough(double actual, double expected)
{
return std::abs(actual - expected) < 1.0e-12;
}

bool expectClose(double actual, double expected, char const* label)
{
if (closeEnough(actual, expected)) {
return true;
}

std::cerr << label << ": expected " << expected << ", got " << actual << "\n";
return false;
}

bool phasorAdvancesByFrequencyOverSampleRate()
{
elem::PhasorNode<double, false> phasor(1, 100.0, 4);

double rate[] = {25.0, 25.0, 25.0, 25.0};
double out[] = {0.0, 0.0, 0.0, 0.0};
double const* inputs[] = {rate};
double* outputs[] = {out};

phasor.process(makeContext(inputs, 1, outputs, 4));

return expectClose(out[0], 0.0, "phasor sample 0")
&& expectClose(out[1], 0.25, "phasor sample 1")
&& expectClose(out[2], 0.5, "phasor sample 2")
&& expectClose(out[3], 0.75, "phasor sample 3");
}

bool syncPhasorResetsOnRisingResetSignal()
{
elem::PhasorNode<double, true> phasor(2, 100.0, 5);

double rate[] = {25.0, 25.0, 25.0, 25.0, 25.0};
double reset[] = {0.0, 0.0, 1.0, 1.0, 0.0};
double out[] = {0.0, 0.0, 0.0, 0.0, 0.0};
double const* inputs[] = {rate, reset};
double* outputs[] = {out};

phasor.process(makeContext(inputs, 2, outputs, 5));

return expectClose(out[0], 0.0, "sync phasor sample 0")
&& expectClose(out[1], 0.25, "sync phasor sample 1")
&& expectClose(out[2], 0.0, "sync phasor sample 2")
&& expectClose(out[3], 0.25, "sync phasor sample 3")
&& expectClose(out[4], 0.5, "sync phasor sample 4");
}

} // namespace

int main()
{
return phasorAdvancesByFrequencyOverSampleRate() && syncPhasorResetsOnRisingResetSignal() ? 0 : 1;
}