From 767c027ddd6aa2e1e12ddb8a661c297e833d6a7a Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Fri, 16 Aug 2024 11:28:42 +0100 Subject: [PATCH] Add Accumulator node --- .../libs/signalflow-stubs/signalflow.pyi | 87 +++++++++++++------ .../signalflow/node/envelope/accumulator.h | 28 ++++++ source/include/signalflow/signalflow.h | 1 + source/src/CMakeLists.txt | 1 + source/src/node/envelope/accumulator.cpp | 54 ++++++++++++ source/src/python/node.cpp | 12 +-- source/src/python/nodes.cpp | 5 +- 7 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 source/include/signalflow/node/envelope/accumulator.h create mode 100644 source/src/node/envelope/accumulator.cpp diff --git a/auxiliary/libs/signalflow-stubs/signalflow.pyi b/auxiliary/libs/signalflow-stubs/signalflow.pyi index 63a86f2f..1e2c8adf 100644 --- a/auxiliary/libs/signalflow-stubs/signalflow.pyi +++ b/auxiliary/libs/signalflow-stubs/signalflow.pyi @@ -10,7 +10,7 @@ from __future__ import annotations import numpy import typing import typing_extensions -__all__ = ['ADSREnvelope', 'ASREnvelope', 'Abs', 'Add', 'AllpassDelay', 'AmplitudeToDecibels', 'AudioGraph', 'AudioGraphConfig', 'AudioIOException', 'AudioIn', 'AudioOut', 'AudioOut_Abstract', 'AudioOut_Dummy', 'AzimuthPanner', 'BeatCutter', 'BiquadFilter', 'Buffer', 'Buffer2D', 'BufferLooper', 'BufferPlayer', 'BufferRecorder', 'CPUUsageAboveLimitException', 'ChannelArray', 'ChannelCrossfade', 'ChannelMixer', 'ChannelPanner', 'ChannelSelect', 'Clip', 'ClockDivider', 'CombDelay', 'Compressor', 'Constant', 'Cos', 'Counter', 'CrossCorrelate', 'DCFilter', 'DecibelsToAmplitude', 'DetectSilence', 'DeviceNotFoundException', 'Divide', 'EQ', 'Envelope', 'EnvelopeBuffer', 'Equal', 'Euclidean', 'FFT', 'FFTBuffer', 'FFTBufferPlayer', 'FFTContinuousPhaseVocoder', 'FFTContrast', 'FFTConvolve', 'FFTCrossFade', 'FFTFindPeaks', 'FFTLFO', 'FFTLPF', 'FFTMagnitudePhaseArray', 'FFTNode', 'FFTNoiseGate', 'FFTOpNode', 'FFTPhaseVocoder', 'FFTRandomPhase', 'FFTScaleMagnitudes', 'FFTTonality', 'FFTTransform', 'FeedbackBufferReader', 'FeedbackBufferWriter', 'FlipFlop', 'Fold', 'FrequencyToMidiNote', 'Gate', 'Granulator', 'GraphAlreadyCreatedException', 'GraphNotCreatedException', 'GreaterThan', 'GreaterThanOrEqual', 'IFFT', 'If', 'Impulse', 'ImpulseSequence', 'Index', 'InsufficientBufferSizeException', 'InvalidChannelCountException', 'KDTree', 'KDTreeMatch', 'LFO', 'Latch', 'LessThan', 'LessThanOrEqual', 'Line', 'Logistic', 'Maximiser', 'MidiNoteToFrequency', 'Modulo', 'MoogVCF', 'MouseDown', 'MouseX', 'MouseY', 'Multiply', 'NearestNeighbour', 'Node', 'NodeAlreadyPlayingException', 'NodeNotPlayingException', 'NodeRegistry', 'NotEqual', 'OneTapDelay', 'OnsetDetector', 'Patch', 'PatchFinishedPlaybackException', 'PatchRegistry', 'PatchSpec', 'PinkNoise', 'Pow', 'RMS', 'RandomBrownian', 'RandomChoice', 'RandomCoin', 'RandomExponential', 'RandomExponentialDist', 'RandomGaussian', 'RandomImpulse', 'RandomImpulseSequence', 'RandomUniform', 'RectangularEnvelope', 'Resample', 'Round', 'RoundToScale', 'SIGNALFLOW_DEFAULT_BLOCK_SIZE', 'SIGNALFLOW_DEFAULT_FFT_HOP_SIZE', 'SIGNALFLOW_DEFAULT_FFT_SIZE', 'SIGNALFLOW_DEFAULT_SAMPLE_RATE', 'SIGNALFLOW_DEFAULT_TRIGGER', 'SIGNALFLOW_EVENT_DISTRIBUTION_POISSON', 'SIGNALFLOW_EVENT_DISTRIBUTION_UNIFORM', 'SIGNALFLOW_FILTER_TYPE_BAND_PASS', 'SIGNALFLOW_FILTER_TYPE_HIGH_PASS', 'SIGNALFLOW_FILTER_TYPE_HIGH_SHELF', 'SIGNALFLOW_FILTER_TYPE_LOW_PASS', 'SIGNALFLOW_FILTER_TYPE_LOW_SHELF', 'SIGNALFLOW_FILTER_TYPE_NOTCH', 'SIGNALFLOW_FILTER_TYPE_PEAK', 'SIGNALFLOW_INTERPOLATION_MODE_COSINE', 'SIGNALFLOW_INTERPOLATION_MODE_LINEAR', 'SIGNALFLOW_INTERPOLATION_MODE_NONE', 'SIGNALFLOW_MAX_CHANNELS', 'SIGNALFLOW_MAX_FFT_SIZE', 'SIGNALFLOW_NODE_BUFFER_SIZE', 'SIGNALFLOW_NODE_INITIAL_OUTPUT_CHANNELS', 'SIGNALFLOW_NODE_STATE_ACTIVE', 'SIGNALFLOW_NODE_STATE_STOPPED', 'SIGNALFLOW_PATCH_STATE_ACTIVE', 'SIGNALFLOW_PATCH_STATE_STOPPED', 'SVFilter', 'SampleAndHold', 'SawLFO', 'SawOscillator', 'ScaleLinExp', 'ScaleLinLin', 'SegmentPlayer', 'SegmentedGranulator', 'Sequence', 'Sin', 'SineLFO', 'SineOscillator', 'Smooth', 'SpatialEnvironment', 'SpatialPanner', 'SpatialSpeaker', 'SquareLFO', 'SquareOscillator', 'Squiz', 'StereoBalance', 'StereoPanner', 'StereoWidth', 'StochasticNode', 'Stutter', 'Subtract', 'Sum', 'Tan', 'Tanh', 'TimeShift', 'TriangleLFO', 'TriangleOscillator', 'TriggerMult', 'TriggerRoundRobin', 'UnknownTriggerNameException', 'VampAnalysis', 'WaveShaper', 'WaveShaperBuffer', 'Wavetable', 'Wavetable2D', 'WavetableBuffer', 'WetDry', 'WhiteNoise', 'Wrap', 'amplitude_to_db', 'clip', 'db_to_amplitude', 'fold', 'frequency_to_midi_note', 'midi_note_to_frequency', 'random_exponential', 'random_integer', 'random_seed', 'random_uniform', 'save_block_to_text_file', 'save_block_to_wav_file', 'scale_exp_lin', 'scale_lin_exp', 'scale_lin_lin', 'signalflow_event_distribution_t', 'signalflow_filter_type_t', 'signalflow_interpolation_mode_t', 'signalflow_node_state_t', 'signalflow_patch_state_t', 'wrap'] +__all__ = ['ADSREnvelope', 'ASREnvelope', 'Abs', 'Accumulator', 'Add', 'AllpassDelay', 'AmplitudeToDecibels', 'AudioGraph', 'AudioGraphConfig', 'AudioIOException', 'AudioIn', 'AudioOut', 'AudioOut_Abstract', 'AudioOut_Dummy', 'AzimuthPanner', 'BeatCutter', 'BiquadFilter', 'Buffer', 'Buffer2D', 'BufferLooper', 'BufferPlayer', 'BufferRecorder', 'CPUUsageAboveLimitException', 'ChannelArray', 'ChannelCrossfade', 'ChannelMixer', 'ChannelPanner', 'ChannelSelect', 'Clip', 'ClockDivider', 'CombDelay', 'Compressor', 'Constant', 'Cos', 'Counter', 'CrossCorrelate', 'DCFilter', 'DecibelsToAmplitude', 'DetectSilence', 'DeviceNotFoundException', 'Divide', 'EQ', 'Envelope', 'EnvelopeBuffer', 'Equal', 'Euclidean', 'FFT', 'FFTBuffer', 'FFTBufferPlayer', 'FFTContinuousPhaseVocoder', 'FFTContrast', 'FFTConvolve', 'FFTCrossFade', 'FFTFindPeaks', 'FFTLFO', 'FFTLPF', 'FFTMagnitudePhaseArray', 'FFTNode', 'FFTNoiseGate', 'FFTOpNode', 'FFTPhaseVocoder', 'FFTRandomPhase', 'FFTScaleMagnitudes', 'FFTTonality', 'FFTTransform', 'FeedbackBufferReader', 'FeedbackBufferWriter', 'FlipFlop', 'Fold', 'FrequencyToMidiNote', 'Gate', 'Granulator', 'GraphAlreadyCreatedException', 'GraphNotCreatedException', 'GreaterThan', 'GreaterThanOrEqual', 'HistoryBufferWriter', 'IFFT', 'If', 'Impulse', 'ImpulseSequence', 'Index', 'InsufficientBufferSizeException', 'InvalidChannelCountException', 'KDTree', 'KDTreeMatch', 'LFO', 'Latch', 'LessThan', 'LessThanOrEqual', 'Line', 'Logistic', 'Maximiser', 'MidiNoteToFrequency', 'Modulo', 'MoogVCF', 'MouseDown', 'MouseX', 'MouseY', 'Multiply', 'NearestNeighbour', 'Node', 'NodeAlreadyPlayingException', 'NodeNotPlayingException', 'NodeRegistry', 'NotEqual', 'OneTapDelay', 'OnsetDetector', 'Patch', 'PatchFinishedPlaybackException', 'PatchRegistry', 'PatchSpec', 'PinkNoise', 'Pow', 'RMS', 'RandomBrownian', 'RandomChoice', 'RandomCoin', 'RandomExponential', 'RandomExponentialDist', 'RandomGaussian', 'RandomImpulse', 'RandomImpulseSequence', 'RandomUniform', 'RectangularEnvelope', 'Resample', 'Round', 'RoundToScale', 'SIGNALFLOW_DEFAULT_BLOCK_SIZE', 'SIGNALFLOW_DEFAULT_FFT_HOP_SIZE', 'SIGNALFLOW_DEFAULT_FFT_SIZE', 'SIGNALFLOW_DEFAULT_SAMPLE_RATE', 'SIGNALFLOW_DEFAULT_TRIGGER', 'SIGNALFLOW_EVENT_DISTRIBUTION_POISSON', 'SIGNALFLOW_EVENT_DISTRIBUTION_UNIFORM', 'SIGNALFLOW_FILTER_TYPE_BAND_PASS', 'SIGNALFLOW_FILTER_TYPE_HIGH_PASS', 'SIGNALFLOW_FILTER_TYPE_HIGH_SHELF', 'SIGNALFLOW_FILTER_TYPE_LOW_PASS', 'SIGNALFLOW_FILTER_TYPE_LOW_SHELF', 'SIGNALFLOW_FILTER_TYPE_NOTCH', 'SIGNALFLOW_FILTER_TYPE_PEAK', 'SIGNALFLOW_INTERPOLATION_MODE_COSINE', 'SIGNALFLOW_INTERPOLATION_MODE_LINEAR', 'SIGNALFLOW_INTERPOLATION_MODE_NONE', 'SIGNALFLOW_MAX_CHANNELS', 'SIGNALFLOW_MAX_FFT_SIZE', 'SIGNALFLOW_NODE_BUFFER_SIZE', 'SIGNALFLOW_NODE_INITIAL_OUTPUT_CHANNELS', 'SIGNALFLOW_NODE_STATE_ACTIVE', 'SIGNALFLOW_NODE_STATE_STOPPED', 'SIGNALFLOW_PATCH_STATE_ACTIVE', 'SIGNALFLOW_PATCH_STATE_STOPPED', 'SVFilter', 'SampleAndHold', 'SawLFO', 'SawOscillator', 'ScaleLinExp', 'ScaleLinLin', 'SegmentPlayer', 'SegmentedGranulator', 'SelectInput', 'Sequence', 'Sin', 'SineLFO', 'SineOscillator', 'Smooth', 'SpatialEnvironment', 'SpatialPanner', 'SpatialSpeaker', 'SquareLFO', 'SquareOscillator', 'Squiz', 'StereoBalance', 'StereoPanner', 'StereoWidth', 'StochasticNode', 'Stutter', 'Subtract', 'Sum', 'Tan', 'Tanh', 'TimeShift', 'TriangleLFO', 'TriangleOscillator', 'TriggerMult', 'TriggerRoundRobin', 'UnknownTriggerNameException', 'VampAnalysis', 'WaveShaper', 'WaveShaperBuffer', 'Wavetable', 'Wavetable2D', 'WavetableBuffer', 'WetDry', 'WhiteNoise', 'Wrap', 'amplitude_to_db', 'clip', 'db_to_amplitude', 'fold', 'frequency_to_midi_note', 'midi_note_to_frequency', 'random_exponential', 'random_integer', 'random_seed', 'random_uniform', 'save_block_to_text_file', 'save_block_to_wav_file', 'scale_exp_lin', 'scale_lin_exp', 'scale_lin_lin', 'signalflow_event_distribution_t', 'signalflow_filter_type_t', 'signalflow_interpolation_mode_t', 'signalflow_node_state_t', 'signalflow_patch_state_t', 'wrap'] class ADSREnvelope(Node): """ Attack-decay-sustain-release envelope. Sustain portion is held until gate is zero. @@ -29,6 +29,12 @@ class Abs(Node): """ def __init__(self, a: Node = 0) -> None: ... +class Accumulator(Node): + """ + Accumulator with decay. + """ + def __init__(self, strike_force: Node = 0.5, decay_coefficient: Node = 0.9999, trigger: Node = None) -> None: + ... class Add(Node): """ Add each sample of a to each sample of b. Can also be written as a + b @@ -299,100 +305,100 @@ class Buffer: """ A buffer of audio samples, containing one or more channels. """ - def __add__(self, value: float) -> typing_extensions.Buffer: + def __add__(self: typing_extensions.Buffer, value: float) -> typing_extensions.Buffer: """ Returns a new Buffer containing the samples in `self` added to `value`. """ - def __div__(self, value: float) -> typing_extensions.Buffer: + def __div__(self: typing_extensions.Buffer, value: float) -> typing_extensions.Buffer: """ Returns a new Buffer containing the samples in `self` divided by `value`. """ - def __getitem__(self, arg0: int) -> typing_extensions.Buffer: + def __getitem__(self: typing_extensions.Buffer, arg0: int) -> typing_extensions.Buffer: ... @typing.overload - def __init__(self) -> None: + def __init__(self: typing_extensions.Buffer) -> None: """ Create a null Buffer with no memory allocated. """ @typing.overload - def __init__(self, filename: str) -> None: + def __init__(self: typing_extensions.Buffer, filename: str) -> None: """ Load a Buffer from an audio file. """ @typing.overload - def __init__(self, num_channels: int, num_frames: int) -> None: + def __init__(self: typing_extensions.Buffer, num_channels: int, num_frames: int) -> None: """ Allocate a buffer with `num_channels` channels and `num_frames` frames. """ @typing.overload - def __init__(self, num_channels: int, num_frames: int, data: list[list[float]]) -> None: + def __init__(self: typing_extensions.Buffer, num_channels: int, num_frames: int, data: list[list[float]]) -> None: """ Allocate a buffer with `num_channels` channels and `num_frames` frames, containing the floating-point samples in `data`. """ @typing.overload - def __init__(self, arg0: list[list[float]]) -> None: + def __init__(self: typing_extensions.Buffer, arg0: list[list[float]]) -> None: """ Allocate a buffer with `num_channels` channels and `num_frames` frames, containing the floating-point samples in `data`. """ @typing.overload - def __init__(self, data: list[float]) -> None: + def __init__(self: typing_extensions.Buffer, data: list[float]) -> None: """ Allocate a buffer containing the floating-point samples in `data`. """ @typing.overload - def __init__(self, function: typing.Callable[[float], float]) -> None: + def __init__(self: typing_extensions.Buffer, function: typing.Callable[[float], float]) -> None: """ Allocate a buffer filled with the output of the function `function`. """ @typing.overload - def __init__(self, num_frames: int, function: typing.Callable[[float], float]) -> None: + def __init__(self: typing_extensions.Buffer, num_frames: int, function: typing.Callable[[float], float]) -> None: """ Allocate a mono buffer with `num_frames` frames, filled with the output of the function `function`. """ @typing.overload - def __init__(self, num_channels: int, num_frames: int, function: typing.Callable[[float], float]) -> None: + def __init__(self: typing_extensions.Buffer, num_channels: int, num_frames: int, function: typing.Callable[[float], float]) -> None: """ Allocate a buffer with `num_channels` channels and `num_frames` frames, filled with the output of the function `function`. """ - def __len__(self) -> int: + def __len__(self: typing_extensions.Buffer) -> int: """ Returns the length of the buffer `self`, in frames. """ - def __mul__(self, value: float) -> typing_extensions.Buffer: + def __mul__(self: typing_extensions.Buffer, value: float) -> typing_extensions.Buffer: """ Returns a new Buffer containing the samples in `self` multiplied by `value`. """ - def __radd__(self, value_: float) -> typing_extensions.Buffer: + def __radd__(self: typing_extensions.Buffer, value_: float) -> typing_extensions.Buffer: """ Returns a new Buffer containing the samples in `self` added to `value`. """ - def __rmul__(self, value_: float) -> typing_extensions.Buffer: + def __rmul__(self: typing_extensions.Buffer, value_: float) -> typing_extensions.Buffer: """ Returns a new Buffer containing the samples in `self` multiplied by `value`. """ - def __str__(self) -> str: + def __str__(self: typing_extensions.Buffer) -> str: ... - def __sub__(self, value: float) -> typing_extensions.Buffer: + def __sub__(self: typing_extensions.Buffer, value: float) -> typing_extensions.Buffer: """ Returns a new Buffer containing the samples in `self` subtracted by `value`. """ @typing.overload - def fill(self, sample: float) -> None: + def fill(self: typing_extensions.Buffer, sample: float) -> None: ... @typing.overload - def fill(self, function: typing.Callable[[float], float]) -> None: + def fill(self: typing_extensions.Buffer, function: typing.Callable[[float], float]) -> None: ... - def get(self, channel: int, frame: float) -> float: + def get(self: typing_extensions.Buffer, channel: int, frame: float) -> float: ... - def get_frame(self, channel: int, frame: float) -> float: + def get_frame(self: typing_extensions.Buffer, channel: int, frame: float) -> float: ... - def load(self, filename: str) -> None: + def load(self: typing_extensions.Buffer, filename: str) -> None: ... - def save(self, filename: str) -> None: + def save(self: typing_extensions.Buffer, filename: str) -> None: ... - def set(self, channel: int, frame: int, value: float) -> bool: + def set(self: typing_extensions.Buffer, channel: int, frame: int, value: float) -> bool: ... - def split(self, num_frames_per_part: int) -> list[typing_extensions.Buffer]: + def split(self: typing_extensions.Buffer, num_frames_per_part: int) -> list[typing_extensions.Buffer]: ... @property def data(self) -> numpy.ndarray[numpy.float32]: @@ -831,6 +837,12 @@ class GreaterThanOrEqual(Node): """ def __init__(self, a: Node = 0, b: Node = 0) -> None: ... +class HistoryBufferWriter(Node): + """ + Writes a rolling history buffer of a given duration. At a given moment in time, the contents of the buffer will be equal to the past N seconds of the audio generated by `input`. This is useful for (e.g.) a visual display of a rolling waveform or LFO window. `downsample` can be used to downsample the input; for example, with `downsample` of 10, a 1-second buffer can be used to display 10 seconds of historical audio. + """ + def __init__(self, buffer: ... = None, input: Node = 0.0, downsample: int = 1) -> None: + ... class IFFT(FFTOpNode): """ Inverse Fast Fourier Transform. Requires an FFT* input, generates a time-domain output. @@ -1627,11 +1639,30 @@ class SegmentedGranulator(Node): """ def __init__(self, buffer: ... = None, onset_times: list[float] = 0, durations: list[float] = 0, index: Node = 0.0, rate: Node = 1.0, clock: Node = 0, max_grains: Node = 2048) -> None: ... +class SelectInput(Node): + """ + Select from the outputs of one or more `inputs` nodes, based on the input index specified in `index`. Unlike `ChannelSelect`, inputs may be multichannel, and `index` can be modulated in real time. + """ + @typing.overload + def __init__(self, index: Node = 0) -> None: + ... + @typing.overload + def __init__(self, inputs: ..., index: Node = 0) -> None: + ... + @typing.overload + def __init__(self, inputs: list[Node], index: Node = 0) -> None: + ... + @typing.overload + def __init__(self, inputs: list[int], index: Node = 0) -> None: + ... + @typing.overload + def __init__(self, inputs: list[float], index: Node = 0) -> None: + ... class Sequence(Node): """ Outputs the elements in `sequence`, incrementing position on each `clock`. """ - def __init__(self, sequence: list[float] = [], clock: Node = None) -> None: + def __init__(self: typing.Sequence, sequence: list[float] = [], clock: Node = None) -> None: ... class Sin(Node): """ diff --git a/source/include/signalflow/node/envelope/accumulator.h b/source/include/signalflow/node/envelope/accumulator.h new file mode 100644 index 00000000..8cbcee43 --- /dev/null +++ b/source/include/signalflow/node/envelope/accumulator.h @@ -0,0 +1,28 @@ +#pragma once + +#include "signalflow/core/constants.h" +#include "signalflow/node/node.h" + +namespace signalflow +{ + +/**--------------------------------------------------------------------------------* + * Accumulator with decay. + *---------------------------------------------------------------------------------*/ +class Accumulator : public Node +{ +public: + Accumulator(NodeRef strike_force = 0.5, NodeRef decay_coefficient = 0.9999, NodeRef trigger = nullptr); + virtual void trigger(std::string name = SIGNALFLOW_DEFAULT_TRIGGER, float value = SIGNALFLOW_NULL_FLOAT) override; + virtual void process(Buffer &out, int num_frames) override; + +private: + NodeRef strike_force; + NodeRef decay_coefficient; + NodeRef _trigger; + double current_value; +}; + +REGISTER(Accumulator, "accumulator") + +} diff --git a/source/include/signalflow/signalflow.h b/source/include/signalflow/signalflow.h index d96724eb..467d4e3d 100644 --- a/source/include/signalflow/signalflow.h +++ b/source/include/signalflow/signalflow.h @@ -119,6 +119,7 @@ /*------------------------------------------------------------------------ * Envelopes and lifecycle *-----------------------------------------------------------------------*/ +#include #include #include #include diff --git a/source/src/CMakeLists.txt b/source/src/CMakeLists.txt index a96a7ec1..1083986d 100644 --- a/source/src/CMakeLists.txt +++ b/source/src/CMakeLists.txt @@ -29,6 +29,7 @@ set(SRC ${SRC} ${CMAKE_CURRENT_SOURCE_DIR}/node/stochastic/random-brownian.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/stochastic/random-coin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/stochastic/random-choice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/node/envelope/accumulator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/envelope/adsr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/envelope/asr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/envelope/envelope.cpp diff --git a/source/src/node/envelope/accumulator.cpp b/source/src/node/envelope/accumulator.cpp new file mode 100644 index 00000000..090cee6a --- /dev/null +++ b/source/src/node/envelope/accumulator.cpp @@ -0,0 +1,54 @@ +#include "signalflow/core/graph.h" +#include "signalflow/node/envelope/accumulator.h" +#include + +namespace signalflow +{ + +Accumulator::Accumulator(NodeRef strike_force, NodeRef decay_coefficient, NodeRef trigger) + : Node(), strike_force(strike_force), decay_coefficient(decay_coefficient), _trigger(trigger) +{ + SIGNALFLOW_CHECK_GRAPH(); + + this->name = "accumulator"; + + this->create_input("strike_force", this->strike_force); + this->create_input("decay_coefficient", this->decay_coefficient); + this->create_input("trigger", this->_trigger); + + if (!_trigger) + { + this->trigger(); + } +} + +void Accumulator::trigger(std::string name, float value) +{ + if (name == SIGNALFLOW_DEFAULT_TRIGGER) + { + float strike_force = value == SIGNALFLOW_NULL_FLOAT ? value : this->strike_force->out[0][0]; + float increment = strike_force * (1.0 - strike_force); + this->current_value += increment; + } +} + +void Accumulator::process(Buffer &out, int num_frames) +{ + for (int frame = 0; frame < num_frames; frame++) + { + if (SIGNALFLOW_CHECK_TRIGGER(_trigger, frame)) + { + float strike_force = this->strike_force->out[0][frame]; + float increment = strike_force * (1.0 - this->current_value); + this->current_value += increment; + } + for (int channel = 0; channel < this->num_output_channels; channel++) + { + out[channel][frame] = this->current_value; + } + float decay_coefficient = this->decay_coefficient->out[0][frame]; + this->current_value *= decay_coefficient; + } +} + +} diff --git a/source/src/python/node.cpp b/source/src/python/node.cpp index 75ac7f15..61315296 100644 --- a/source/src/python/node.cpp +++ b/source/src/python/node.cpp @@ -197,11 +197,13 @@ void init_python_node(py::module &m) .def_property_readonly( "output_buffer", [](Node &node) { /*-------------------------------------------------------------------------------- - * Assigning a data owner to the array ensures that it is returned as a - * pointer to the original data, rather than a copy. This means that we can - * modify the contents of the output buffer in-place from Python if we want to. - * https://github.com/pybind/pybind11/issues/323 - *-------------------------------------------------------------------------------*/ + * Assigning a data owner to the array ensures that it is returned as a + * pointer to the original data, rather than a copy. This means that we can + * modify the contents of the output buffer in-place from Python if we want to. + * https://github.com/pybind/pybind11/issues/323 + * + * TODO: Why not return the actual Buffer object? + *-------------------------------------------------------------------------------*/ py::str dummy_data_owner; return py::array_t( { node.get_num_output_channels_allocated(), node.last_num_frames }, diff --git a/source/src/python/nodes.cpp b/source/src/python/nodes.cpp index 66a3fc79..bf542257 100644 --- a/source/src/python/nodes.cpp +++ b/source/src/python/nodes.cpp @@ -84,6 +84,9 @@ void init_python_nodes(py::module &m) #endif + py::class_>(m, "Accumulator", "Accumulator with decay.") + .def(py::init(), "strike_force"_a = 0.5, "decay_coefficient"_a = 0.9999, "trigger"_a = nullptr); + py::class_>(m, "ADSREnvelope", "Attack-decay-sustain-release envelope. Sustain portion is held until gate is zero.") .def(py::init(), "attack"_a = 0.1, "decay"_a = 0.1, "sustain"_a = 0.5, "release"_a = 0.1, "gate"_a = 0); @@ -237,7 +240,7 @@ void init_python_nodes(py::module &m) py::class_>(m, "ScaleLinLin", "Scales the input from a linear range (between a and b) to a linear range (between c and d).") .def(py::init(), "input"_a = 0, "a"_a = 0, "b"_a = 1, "c"_a = 1, "d"_a = 10); - py::class_>(m, "SelectInput", "Selects an input") + py::class_>(m, "SelectInput", "Select from the outputs of one or more `inputs` nodes, based on the input index specified in `index`. Unlike `ChannelSelect`, inputs may be multichannel, and `index` can be modulated in real time.") .def(py::init(), "index"_a = 0) .def(py::init, NodeRef>(), "inputs"_a, "index"_a = 0) .def(py::init, NodeRef>(), "inputs"_a, "index"_a = 0)