diff --git a/benchmarks/BM_random.cpp b/benchmarks/BM_random.cpp index e69de29b..85890887 100644 --- a/benchmarks/BM_random.cpp +++ b/benchmarks/BM_random.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "MathHelpers.h" +#include +#include +#include + +class RandomFill : public benchmark::Fixture { +public: + void SetUp(const ::benchmark::State& state) { + output = std::vector(state.range(0)); + } + + void TearDown(const ::benchmark::State& state) { + UNUSED(state); + } + + std::vector output; +}; + +BENCHMARK_DEFINE_F(RandomFill, StdRandom)(benchmark::State& state) { + std::minstd_rand prng; + std::uniform_real_distribution dist(0.0f, 1.0f); + + for (auto _ : state) + { + for (float &out : output) + out = dist(prng); + } +} + +BENCHMARK_DEFINE_F(RandomFill, FastRandom)(benchmark::State& state) { + fast_rand prng; + fast_real_distribution dist(0.0f, 1.0f); + + for (auto _ : state) + { + for (float &out : output) + out = dist(prng); + } +} +BENCHMARK_DEFINE_F(RandomFill, StdRandomBipolar)(benchmark::State& state) { + std::minstd_rand prng; + std::uniform_real_distribution dist(-1.0f, 1.0f); + + for (auto _ : state) + { + for (float &out : output) + out = dist(prng); + } +} + +BENCHMARK_DEFINE_F(RandomFill, FastRandomBipolar)(benchmark::State& state) { + fast_rand prng; + fast_real_distribution dist(-1.0f, 1.0f); + + for (auto _ : state) + { + for (float &out : output) + out = dist(prng); + } +} + +BENCHMARK_REGISTER_F(RandomFill, StdRandom)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(RandomFill, StdRandomBipolar)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(RandomFill, FastRandom)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); +BENCHMARK_REGISTER_F(RandomFill, FastRandomBipolar)->RangeMultiplier(4)->Range(1 << 2, 1 << 12); diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 1b2fcdbe..53aa801a 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -64,6 +64,7 @@ sfizz_add_benchmark(bm_pointerIterationOrOffsets BM_pointerIterationOrOffsets.cp sfizz_add_benchmark(bm_maps BM_maps.cpp) target_link_libraries(bm_maps PRIVATE absl::flat_hash_map) sfizz_add_benchmark(bm_mapVsArray BM_mapVsArray.cpp) +sfizz_add_benchmark(bm_random BM_random.cpp) sfizz_add_benchmark(bm_logger BM_logger.cpp) target_link_libraries(bm_logger PRIVATE sfizz::sfizz) diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index 88116dab..3bb10f67 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -14,8 +14,10 @@ #include "SIMDConfig.h" #include "absl/types/span.h" #include -#include #include +#include +#include +#include #include #if SFIZZ_HAVE_SSE #include @@ -109,17 +111,6 @@ constexpr Type mag2db(Type in) return static_cast(20.0) * std::log10(in); } -/** - * @brief Global random singletons - * - * TODO: could be moved into a singleton class holder - * - */ -namespace Random { -static std::random_device randomDevice; -static std::minstd_rand randomGenerator { randomDevice() }; -} // namespace Random - /** * @brief Converts a midi note to a frequency value * @@ -504,3 +495,108 @@ public: private: const int savedFloatMode; }; + +/** + * @brief A low-quality random number generator guaranteed to be very fast + */ +class fast_rand { +public: + typedef uint32_t result_type; + + fast_rand() noexcept + { + } + + explicit fast_rand(uint32_t value) noexcept + : mem(value) + { + } + + static constexpr uint32_t min() noexcept + { + return 0; + } + + static constexpr uint32_t max() noexcept + { + return std::numeric_limits::max(); + } + + uint32_t operator()() noexcept + { + uint32_t next = mem * 1664525u + 1013904223u; // Numerical Recipes + mem = next; + return next; + } + + void seed(uint32_t value = 0) noexcept + { + mem = value; + } + + void discard(unsigned long long z) noexcept + { + for (unsigned long long i = 0; i < z; ++i) + operator()(); + } + +private: + uint32_t mem = 0; +}; + +/** + * @brief A uniform real distribution guaranteed to be very fast + */ +template +class fast_real_distribution { +public: + static_assert(std::is_floating_point::value, "The type must be floating point."); + + typedef T result_type; + + fast_real_distribution(T a, T b) + : a_(a), b_(b), k_(b - a) + { + } + + template + T operator()(G& g) const + { + return a_ + (g() - T(G::min())) * (k_ / T(G::max() - G::min())); + } + + T a() const noexcept + { + return a_; + } + + T b() const noexcept + { + return b_; + } + + T min() const noexcept + { + return a_; + } + + T max() const noexcept + { + return b_; + } + +private: + T a_; + T b_; + T k_; +}; + +/** + * @brief Global random singletons + * + * TODO: could be moved into a singleton class holder + * + */ +namespace Random { +static fast_rand randomGenerator; +} // namespace Random diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index dd032c8f..71508b55 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1036,7 +1036,7 @@ float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept { - std::uniform_real_distribution volumeDistribution { -ampRandom, ampRandom }; + fast_real_distribution volumeDistribution { -ampRandom, ampRandom }; auto baseVolumedB = volume + volumeDistribution(Random::randomGenerator); if (trigger == SfzTrigger::release || trigger == SfzTrigger::release_key) baseVolumedB -= rtDecay * midiState.getNoteDuration(noteNumber); @@ -1055,7 +1055,7 @@ float sfz::Region::getPhase() const noexcept phase = oscillatorPhase * (1.0f / 360.0f); phase -= static_cast(static_cast(phase)); } else { - std::uniform_real_distribution phaseDist { 0.0001f, 0.9999f }; + fast_real_distribution phaseDist { 0.0001f, 0.9999f }; phase = phaseDist(Random::randomGenerator); } return phase; @@ -1072,7 +1072,7 @@ uint64_t sfz::Region::getOffset(Oversampling factor) const noexcept float sfz::Region::getDelay() const noexcept { - std::uniform_real_distribution delayDistribution { 0, delayRandom }; + fast_real_distribution delayDistribution { 0, delayRandom }; return delay + delayDistribution(Random::randomGenerator); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c71cee5b..0fbb2d6d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,7 @@ set(SFIZZ_TEST_SOURCES CurveT.cpp RegionTriggersT.cpp FloatHelpersT.cpp + RandomHelpersT.cpp WavetablesT.cpp SemaphoreT.cpp TuningT.cpp diff --git a/tests/RandomHelpersT.cpp b/tests/RandomHelpersT.cpp new file mode 100644 index 00000000..9c76fa80 --- /dev/null +++ b/tests/RandomHelpersT.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "catch2/catch.hpp" +#include "sfizz/MathHelpers.h" +#include + +/** + * @brief Check the behavior of a uniform real random generator + * + * - ensure all results to be in range [min;max] + * - ensure to have at least one element in every division of the result range + */ +template +static bool randomTest(float min, float max, unsigned gen, unsigned div) +{ + fast_rand prng; + fast_real_distribution dist(min, max); + std::vector counts(div); + + for (unsigned i = 0; i < gen; ++i) { + float r = dist(prng); + if (r < min || r > max) + return false; + unsigned d = clamp(div * (r - min) / (max - min), 0, div - 1); + ++counts[d]; + } + + for (unsigned count : counts) { + if (count == 0) + return false; + } + return true; +} + +TEST_CASE("[Random] Fast random generation") +{ + unsigned numGenerations = 1000; + unsigned numDivisions = 128; + + auto& test = randomTest>; + + REQUIRE(test(0.0f, 1.0f, numGenerations, numDivisions)); + REQUIRE(test(-1.0f, 1.0f, numGenerations, numDivisions)); + REQUIRE(test(0.0f, 123.0f, numGenerations, numDivisions)); + REQUIRE(test(-123.0f, 0.0f, numGenerations, numDivisions)); +}