diff --git a/ceammc/CHANGELOG.md b/ceammc/CHANGELOG.md index 65e60bd6de..5547f0719a 100644 --- a/ceammc/CHANGELOG.md +++ b/ceammc/CHANGELOG.md @@ -3,6 +3,7 @@ ## [0.9.8] ### Added: - new objects: + - flow.histogram (with flow.hist alias) - for runtime flow histogram calculation - hw.gamepad - gamepad support - hw.printer - simple printing support (only PDF files) - msg.unpack - unpack message to selector and arguments diff --git a/ceammc/ext/src/flow/CMakeLists.txt b/ceammc/ext/src/flow/CMakeLists.txt index c46e49b0a0..89f533056a 100644 --- a/ceammc/ext/src/flow/CMakeLists.txt +++ b/ceammc/ext/src/flow/CMakeLists.txt @@ -39,6 +39,7 @@ ceammc_flow_external(gate) ceammc_flow_external(greater) ceammc_flow_external(greater_eq) ceammc_flow_external(group) +ceammc_flow_external(histogram) ceammc_flow_external(interval) ceammc_flow_external(less) ceammc_flow_external(less_eq) diff --git a/ceammc/ext/src/flow/flow_histogram.cpp b/ceammc/ext/src/flow/flow_histogram.cpp new file mode 100644 index 0000000000..7e40d53524 --- /dev/null +++ b/ceammc/ext/src/flow/flow_histogram.cpp @@ -0,0 +1,137 @@ +/***************************************************************************** + * Copyright 2024 Serge Poltavski. All rights reserved. + * + * This file may be distributed under the terms of GNU Public License version + * 3 (GPL v3) as defined by the Free Software Foundation (FSF). A copy of the + * license should have been included with this file, or the project in which + * this file belongs to. You may also find the details of GPL v3 at: + * http://www.gnu.org/licenses/gpl-3.0.txt + * + * If you have any questions regarding the use of this file, feel free to + * contact the author of this file, or the owner of the project in which + * this file belongs to. + *****************************************************************************/ +#include "flow_histogram.h" +#include "ceammc_factory.h" + +#include +#include + +FlowHistogram::FlowHistogram(const PdArgs& args) + : BaseObject(args) +{ + normalize_ = new BoolProperty("@norm", true); + addProperty(normalize_); + + inner_bins_ = new BoolProperty("@inner_bins", true); + addProperty(inner_bins_); + + sync_ = new BoolProperty("@sync", true); + addProperty(sync_); + + bins_ = new ListProperty("@bins", AtomList { 0. }); + bins_->setArgIndex(0); + bins_->setListCheckFn([this](const AtomListView& lv) { + return lv.size() > 0 && lv.allOf(isFloat); + }); + bins_->setSuccessFn([this](Property* p) { + auto& val = bins_->value(); + val.sort(); + + counters_.assign(val.size() + 1, 0.0); + output_.resizePad(counters_.size(), 0.); + output_.fill(0.0); + + fbins_.resize(val.size() + 2); + fbins_.front() = std::numeric_limits::lowest(); + fbins_.back() = std::numeric_limits::max(); + + for (size_t i = 0; i < val.size(); i++) { + fbins_[i + 1] = val[i].asT(); + } + }); + addProperty(bins_); + + createOutlet(); +} + +void FlowHistogram::onBang() +{ + output(); +} + +void FlowHistogram::onFloat(t_float f) +{ + if (fbins_.empty()) { + return; + } + + // find bin index + // O=log2(N)+O(1) + auto it = std::upper_bound(fbins_.begin(), fbins_.end(), f); + // should not happen + if (it == fbins_.end()) + return; + + // bin index + auto idx = (it - fbins_.begin()) - 1; + + // should not happen + if (idx < 0 || idx >= counters_.size()) + return; + + counters_[idx]++; + output_[idx] = counters_[idx]; + + if (sync_->value()) + output(); +} + +void FlowHistogram::m_clear(t_symbol*, const AtomListView&) +{ + clear(); +} + +void FlowHistogram::clear() +{ + std::fill(counters_.begin(), counters_.end(), 0.0); + output_.fill(0.0); +} + +void FlowHistogram::output() +{ + if (normalize_->value()) { + // O=O(N) max(N−1,0) + std::uint64_t max = 0; + if (inner_bins_->value()) { + if (counters_.size() >= 2) { + auto it = std::max_element(counters_.begin() + 1, counters_.end() - 1); + if (it != counters_.end()) + max = *it; + } + } else { + auto it = std::max_element(counters_.begin(), counters_.end()); + if (it != counters_.end()) + max = *it; + } + + if (max > 0) { + // O=O(N) + for (size_t i = 0; i < counters_.size(); i++) + output_[i] = static_cast(counters_[i]) / max; + } + } + + if (inner_bins_->value()) + listTo(0, output_.view(1, output_.size() - 2)); + else + listTo(0, output_); +} + +void setup_flow_histogram() +{ + ObjectFactory obj("flow.histogram"); + obj.addAlias("flow.hist"); + + obj.addMethod("clear", &FlowHistogram::m_clear); +} diff --git a/ceammc/ext/src/flow/flow_histogram.h b/ceammc/ext/src/flow/flow_histogram.h new file mode 100644 index 0000000000..a9e017736e --- /dev/null +++ b/ceammc/ext/src/flow/flow_histogram.h @@ -0,0 +1,45 @@ +/***************************************************************************** + * Copyright 2024 Serge Poltavski. All rights reserved. + * + * This file may be distributed under the terms of GNU Public License version + * 3 (GPL v3) as defined by the Free Software Foundation (FSF). A copy of the + * license should have been included with this file, or the project in which + * this file belongs to. You may also find the details of GPL v3 at: + * http://www.gnu.org/licenses/gpl-3.0.txt + * + * If you have any questions regarding the use of this file, feel free to + * contact the author of this file, or the owner of the project in which + * this file belongs to. + *****************************************************************************/ +#ifndef FLOW_HISTOGRAM_H +#define FLOW_HISTOGRAM_H + +#include "ceammc_object.h" +using namespace ceammc; + +class FlowHistogram : public BaseObject { + std::vector counters_; + std::vector fbins_; + AtomList output_; + + BoolProperty* normalize_ { nullptr }; + BoolProperty* inner_bins_ { nullptr }; + BoolProperty* sync_ { nullptr }; + ListProperty* bins_ { nullptr }; + +public: + FlowHistogram(const PdArgs& args); + + void onBang() final; + void onFloat(t_float f) final; + + void m_clear(t_symbol*, const AtomListView&); + +private: + void clear(); + void output(); +}; + +void setup_flow_histogram(); + +#endif // FLOW_HISTOGRAM_H diff --git a/ceammc/ext/src/flow/mod_flow.cpp b/ceammc/ext/src/flow/mod_flow.cpp index c39839f0b9..b9937147a4 100644 --- a/ceammc/ext/src/flow/mod_flow.cpp +++ b/ceammc/ext/src/flow/mod_flow.cpp @@ -42,6 +42,7 @@ void setup_flow_append(); void setup_flow_delay(); +void setup_flow_histogram(); void setup_flow_match(); void setup_flow_pipe(); void setup_flow_prepend(); @@ -50,18 +51,18 @@ void setup_flow_space(); void setup_seq_arp(); void setup_seq_bangs(); void setup_seq_counter(); +void setup_seq_life(); void setup_seq_matrix(); void setup_seq_nbangs(); void setup_seq_phasor(); void setup_seq_sequencer(); void setup_seq_toggles(); -void setup_seq_life(); void setup_route_any(); void setup_route_bang(); void setup_route_cycle(); -void setup_route_float(); void setup_route_data(); +void setup_route_float(); void setup_route_list(); void setup_route_prop(); void setup_route_random(); @@ -82,11 +83,11 @@ void ceammc_flow_setup() setup_flow_greater(); setup_flow_greater_eq(); setup_flow_group(); + setup_flow_histogram(); setup_flow_interval(); setup_flow_less(); setup_flow_less_eq(); setup_flow_list2many(); - setup_flow_ring(); setup_flow_loop(); setup_flow_match(); setup_flow_mem(); @@ -104,6 +105,7 @@ void ceammc_flow_setup() setup_flow_record(); setup_flow_reject(); setup_flow_reject_if(); + setup_flow_ring(); setup_flow_route(); setup_flow_select(); setup_flow_seqdelay(); @@ -118,18 +120,18 @@ void ceammc_flow_setup() setup_seq_arp(); setup_seq_bangs(); setup_seq_counter(); + setup_seq_life(); setup_seq_matrix(); setup_seq_nbangs(); setup_seq_phasor(); setup_seq_sequencer(); setup_seq_toggles(); - setup_seq_life(); setup_route_any(); setup_route_bang(); setup_route_cycle(); - setup_route_float(); setup_route_data(); + setup_route_float(); setup_route_list(); setup_route_prop(); setup_route_random(); diff --git a/ceammc/ext/tests/flow/CMakeLists.txt b/ceammc/ext/tests/flow/CMakeLists.txt index cbbf2b9c45..7ff0ff437d 100644 --- a/ceammc/ext/tests/flow/CMakeLists.txt +++ b/ceammc/ext/tests/flow/CMakeLists.txt @@ -36,6 +36,7 @@ add_flow_test(dollar) add_flow_test(dup) add_flow_test(gate) add_flow_test(group) +add_flow_test(histogram) add_flow_test(less) add_flow_test(less_eq) add_flow_test(match) diff --git a/ceammc/ext/tests/flow/test_flow_histogram.cpp b/ceammc/ext/tests/flow/test_flow_histogram.cpp new file mode 100644 index 0000000000..767a7bc051 --- /dev/null +++ b/ceammc/ext/tests/flow/test_flow_histogram.cpp @@ -0,0 +1,290 @@ +/***************************************************************************** + * Copyright 2017 Serge Poltavsky. All rights reserved. + * + * This file may be distributed under the terms of GNU Public License version + * 3 (GPL v3) as defined by the Free Software Foundation (FSF). A copy of the + * license should have been included with this file, or the project in which + * this file belongs to. You may also find the details of GPL v3 at: + * http://www.gnu.org/licenses/gpl-3.0.txt + * + * If you have any questions regarding the use of this file, feel free to + * contact the author of this file, or the owner of the project in which + * this file belongs to. + *****************************************************************************/ +#include "flow_histogram.h" +#include "test_flow_base.h" + +PD_COMPLETE_TEST_SETUP(FlowHistogram, flow, histogram) + +TEST_CASE("flow.histogram", "[externals]") +{ + pd_test_init(); + + SECTION("bounds") + { + float data[] = { 1.0, 2.0, 3.0, 4.0 }; + float* x = nullptr; + + x = std::lower_bound(std::begin(data), std::end(data), 0); + // 1 >= 0 + REQUIRE(*x == 1.0); + x = std::lower_bound(std::begin(data), std::end(data), 0.999); + // 1 >= 0.999 + REQUIRE(*x == 1.0); + x = std::lower_bound(std::begin(data), std::end(data), 1); + // 1 >= 1 + REQUIRE(*x == 1.0); + x = std::lower_bound(std::begin(data), std::end(data), 1.0001); + // 1.001 >= 1 + REQUIRE(*x == 2.0); + } + + SECTION("init") + { + SECTION("default") + { + TExt t("flow.histogram"); + REQUIRE(t.numInlets() == 1); + REQUIRE(t.numOutlets() == 1); + REQUIRE_PROPERTY_FLOAT(t, @norm, 1); + REQUIRE_PROPERTY_FLOAT(t, @sync, 1); + REQUIRE_PROPERTY_FLOAT(t, @inner_bins, 1); + REQUIRE_PROPERTY_LIST(t, @bins, LF(0)); + } + + SECTION("alias") + { + TExt t("flow.hist"); + REQUIRE(t.numInlets() == 1); + } + } + + SECTION("@bins") + { + TExt t("flow.histogram"); + + REQUIRE_FALSE(t->setProperty("@bins", L())); + REQUIRE_FALSE(t->setProperty("@bins", LA("A"))); + REQUIRE_FALSE(t->setProperty("@bins", LA(1, 2, 3, "A"))); + REQUIRE(t->setProperty("@bins", LF(1))); + REQUIRE(t->setProperty("@bins", LF(1, 2, 3))); + REQUIRE(t->setProperty("@bins", LF(3, 2, 1))); + REQUIRE_PROPERTY_LIST(t, @bins, LF(1, 2, 3)); + } + + SECTION("process") + { + SECTION("@bins [1]") + { + TExt t("flow.histogram", LA(1, "@norm", 0.)); + REQUIRE_PROPERTY_LIST(t, @bins, LF(1)); + t << 0.0; + REQUIRE(listAt(t, 0_out) == L()); + t << 1; + REQUIRE(listAt(t, 0_out) == L()); + t << 2; + REQUIRE(listAt(t, 0_out) == L()); + + t->setProperty("@inner_bins", A(0.0)); + + t <<= LA("clear"); + t << 0.0; + REQUIRE(listAt(t, 0_out) == LF(1, 0)); + t << 0.999; + REQUIRE(listAt(t, 0_out) == LF(2, 0)); + t << 1; + REQUIRE(listAt(t, 0_out) == LF(2, 1)); + t << 2; + REQUIRE(listAt(t, 0_out) == LF(2, 2)); + } + + SECTION("@bins [0 1]") + { + TExt t("flow.histogram", LA(0., 1, "@norm", 0.)); + REQUIRE_PROPERTY_LIST(t, @bins, LF(0, 1)); + + t << -1; + REQUIRE(floatAt(t, 0_out) == 0); + t << -0.00001; + REQUIRE(floatAt(t, 0_out) == 0); + t << -0.0; + REQUIRE(floatAt(t, 0_out) == 1); + t << +0.0; + REQUIRE(floatAt(t, 0_out) == 2); + t << 0.99999; + REQUIRE(floatAt(t, 0_out) == 3); + t << 1; + REQUIRE(floatAt(t, 0_out) == 3); + + t->setProperty("@inner_bins", A(0.0)); + t <<= LA("clear"); + + t << -1; + REQUIRE(listAt(t, 0_out) == LF(1, 0, 0)); + t << -0.00001; + REQUIRE(listAt(t, 0_out) == LF(2, 0, 0)); + t << -0.0; + REQUIRE(listAt(t, 0_out) == LF(2, 1, 0)); + t << +0.0; + REQUIRE(listAt(t, 0_out) == LF(2, 2, 0)); + t << 0.99999; + REQUIRE(listAt(t, 0_out) == LF(2, 3, 0)); + t << 1; + REQUIRE(listAt(t, 0_out) == LF(2, 3, 1)); + } + + SECTION("@bins [-1 0 1]") + { + TExt t("flow.histogram", LA(-1, 0., 1, "@norm", 0.)); + REQUIRE_PROPERTY_LIST(t, @bins, LF(-1, 0, 1)); + + t << -1.9999; + REQUIRE(listAt(t, 0_out) == LF(0, 0)); + t << -1; + REQUIRE(listAt(t, 0_out) == LF(1, 0)); + t << -0.0001; + REQUIRE(listAt(t, 0_out) == LF(2, 0)); + t << -0.0; + REQUIRE(listAt(t, 0_out) == LF(2, 1)); + t << 0.0; + REQUIRE(listAt(t, 0_out) == LF(2, 2)); + t << 0.00001; + REQUIRE(listAt(t, 0_out) == LF(2, 3)); + t << 0.99999; + REQUIRE(listAt(t, 0_out) == LF(2, 4)); + t << 1; + REQUIRE(listAt(t, 0_out) == LF(2, 4)); + + t->setProperty("@inner_bins", A(0.0)); + t <<= LA("clear"); + + t << -1.9999; + REQUIRE(listAt(t, 0_out) == LF(1, 0, 0, 0)); + t << -1; + REQUIRE(listAt(t, 0_out) == LF(1, 1, 0, 0)); + t << -0.0001; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 0, 0)); + t << -0.0; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 1, 0)); + t << 0.0; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 2, 0)); + t << 0.00001; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 3, 0)); + t << 0.99999; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 4, 0)); + t << 1; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 4, 1)); + t << 1000; + REQUIRE(listAt(t, 0_out) == LF(1, 2, 4, 2)); + } + + SECTION("@sync 1") + { + TExt t("flow.histogram", LA(-1, 0., 1, "@sync", 1)); + t << -1; + REQUIRE(listAt(t, 0_out) == LF(1, 0)); + } + + SECTION("@sync 0") + { + TExt t("flow.histogram", LA(-1, 0., 1, "@sync", 0., "@norm", 0.)); + t << -1; + REQUIRE_FALSE(t.hasOutputAt(0)); + t << -0.5; + REQUIRE_FALSE(t.hasOutputAt(0)); + t.bang(); + REQUIRE(listAt(t, 0_out) == LF(2, 0)); + } + + SECTION("@norm") + { + SECTION("@inner_bins 1") + { + TExt t("flow.histogram", LA(1, "@norm", 1, "@inner_bins", 1)); + + t << 0.0; + REQUIRE(listAt(t, 0_out) == L()); + t << 1; + REQUIRE(listAt(t, 0_out) == L()); + t << 2; + REQUIRE(listAt(t, 0_out) == L()); + + REQUIRE(t->setProperty("@bins", LF(100, 200))); + t.bang(); + REQUIRE(floatAt(t) == 0); + t << 99; + REQUIRE(floatAt(t) == 0); + t << 99; + REQUIRE(floatAt(t) == 0); + t << 100; + REQUIRE(floatAt(t) == 1); + t << 100; + REQUIRE(floatAt(t) == 1); + t << 150; + REQUIRE(floatAt(t) == 1); + t << 200; + REQUIRE(floatAt(t) == 1); + + REQUIRE(t->setProperty("@bins", LF(1, 2, 3))); + t << 0.0; + REQUIRE(listAt(t) == LF(0, 0)); + t << 1; + REQUIRE(listAt(t) == LF(1, 0)); + t << 1; + REQUIRE(listAt(t) == LF(1, 0)); + t << 2; + REQUIRE(listAt(t) == LF(1, 0.5)); + t << 2.1; + REQUIRE(listAt(t) == LF(1, 1)); + t << -1000; + REQUIRE(listAt(t) == LF(1, 1)); + t << 3; + REQUIRE(listAt(t) == LF(1, 1)); + } + + SECTION("@inner_bins 0") + { + TExt t("flow.histogram", LA(1, "@norm", 1, "@inner_bins", 0.)); + + t << 0.0; + REQUIRE(listAt(t, 0_out) == LF(1, 0)); + t << 1; + REQUIRE(listAt(t, 0_out) == LF(1, 1)); + t << 2; + REQUIRE(listAt(t, 0_out) == LF(0.5, 1)); + + REQUIRE(t->setProperty("@bins", LF(100, 200))); + + t << 99; + REQUIRE(listAt(t, 0_out) == LF(1, 0, 0)); + t << 99; + REQUIRE(listAt(t, 0_out) == LF(1, 0, 0)); + t << 100; + REQUIRE(listAt(t, 0_out) == LF(1, 0.5, 0)); + t << 199; + REQUIRE(listAt(t, 0_out) == LF(1, 1, 0)); + t << 200; + REQUIRE(listAt(t, 0_out) == LF(1, 1, 0.5)); + t << 201; + REQUIRE(listAt(t, 0_out) == LF(1, 1, 1)); + + REQUIRE(t->setProperty("@bins", LF(1, 2, 3))); + + t << 0.0; + REQUIRE(listAt(t) == LF(1, 0, 0, 0)); + t << 1; + REQUIRE(listAt(t) == LF(1, 1, 0, 0)); + t << 2; + REQUIRE(listAt(t) == LF(1, 1, 1, 0)); + t << 2.1; + REQUIRE(listAt(t) == LF(0.5, 0.5, 1, 0)); + t << -1000; + REQUIRE(listAt(t) == LF(1, 0.5, 1, 0)); + t << 3; + REQUIRE(listAt(t) == LF(1, 0.5, 1, 0.5)); + t << 300; + REQUIRE(listAt(t) == LF(1, 0.5, 1, 1)); + } + } + } +}