diff --git a/docs/library/oscillators/sawoscillator/index.md b/docs/library/oscillators/sawoscillator/index.md index 7effa1a1..2698a9b7 100644 --- a/docs/library/oscillators/sawoscillator/index.md +++ b/docs/library/oscillators/sawoscillator/index.md @@ -6,7 +6,7 @@ description: SawOscillator: Produces a (non-band-limited) sawtooth wave, with th # SawOscillator ```python -SawOscillator(frequency=440, phase=None, reset=None) +SawOscillator(frequency=440, phase_offset=None, reset=None) ``` Produces a (non-band-limited) sawtooth wave, with the given `frequency` and `phase` offset. When a `reset` or trigger is received, resets the phase to zero. diff --git a/docs/library/oscillators/sineoscillator/index.md b/docs/library/oscillators/sineoscillator/index.md index e6938abc..550a9129 100644 --- a/docs/library/oscillators/sineoscillator/index.md +++ b/docs/library/oscillators/sineoscillator/index.md @@ -6,7 +6,7 @@ description: SineOscillator: Produces a sine wave at the given `frequency`. # SineOscillator ```python -SineOscillator(frequency=440) +SineOscillator(frequency=440, phase_offset=None, reset=None) ``` Produces a sine wave at the given `frequency`. diff --git a/docs/library/oscillators/wavetable/index.md b/docs/library/oscillators/wavetable/index.md index e4ad42ae..27911f52 100644 --- a/docs/library/oscillators/wavetable/index.md +++ b/docs/library/oscillators/wavetable/index.md @@ -6,7 +6,7 @@ description: Wavetable: Plays the wavetable stored in buffer at the given `frequ # Wavetable ```python -Wavetable(buffer=None, frequency=440, phase=0, sync=0, phase_map=None) +Wavetable(buffer=None, frequency=440, phase_offset=0, sync=0, phase_map=None) ``` Plays the wavetable stored in buffer at the given `frequency` and `phase` offset. `sync` can be used to provide a hard sync input, which resets the wavetable's phase at each zero-crossing. diff --git a/docs/library/oscillators/wavetable2d/index.md b/docs/library/oscillators/wavetable2d/index.md index 4ac62354..5cf7f68e 100644 --- a/docs/library/oscillators/wavetable2d/index.md +++ b/docs/library/oscillators/wavetable2d/index.md @@ -6,7 +6,7 @@ description: Wavetable2D: Wavetable2D # Wavetable2D ```python -Wavetable2D(buffer=None, frequency=440, crossfade=0.0, phase=0.0, sync=0) +Wavetable2D(buffer=None, frequency=440, crossfade=0.0, phase_offset=0.0, sync=0) ``` Wavetable2D diff --git a/source/include/signalflow/node/oscillators/saw.h b/source/include/signalflow/node/oscillators/saw.h index c580ed0f..b2cb2c49 100644 --- a/source/include/signalflow/node/oscillators/saw.h +++ b/source/include/signalflow/node/oscillators/saw.h @@ -11,18 +11,18 @@ namespace signalflow class SawOscillator : public Node { public: - SawOscillator(NodeRef frequency = 440, NodeRef phase = nullptr, NodeRef reset = nullptr); + SawOscillator(NodeRef frequency = 440, NodeRef phase_offset = nullptr, NodeRef reset = nullptr); virtual void alloc() override; virtual void process(Buffer &out, int num_frames) override; virtual void trigger(std::string name = SIGNALFLOW_DEFAULT_TRIGGER, float value = 1.0) override; NodeRef frequency; - NodeRef phase; + NodeRef phase_offset; NodeRef reset; private: - std::vector phase_offset; + std::vector phase; }; REGISTER(SawOscillator, "saw") diff --git a/source/include/signalflow/node/oscillators/sine.h b/source/include/signalflow/node/oscillators/sine.h index 2ea74719..1316fb98 100644 --- a/source/include/signalflow/node/oscillators/sine.h +++ b/source/include/signalflow/node/oscillators/sine.h @@ -10,12 +10,14 @@ namespace signalflow class SineOscillator : public Node { public: - SineOscillator(NodeRef frequency = 440); + SineOscillator(NodeRef frequency = 440, NodeRef phase_offset = nullptr, NodeRef reset = nullptr); virtual void process(Buffer &out, int num_frames) override; virtual void alloc() override; NodeRef frequency; + NodeRef phase_offset; + NodeRef reset; private: std::vector phase; diff --git a/source/include/signalflow/node/oscillators/wavetable.h b/source/include/signalflow/node/oscillators/wavetable.h index 62fdfcc9..6f08abd9 100644 --- a/source/include/signalflow/node/oscillators/wavetable.h +++ b/source/include/signalflow/node/oscillators/wavetable.h @@ -16,7 +16,7 @@ class Wavetable : public Node public: Wavetable(BufferRef buffer = nullptr, NodeRef frequency = 440, - NodeRef phase = 0, + NodeRef phase_offset = 0, NodeRef sync = 0, BufferRef phase_map = nullptr); @@ -26,7 +26,7 @@ class Wavetable : public Node private: BufferRef buffer; NodeRef frequency; - NodeRef phase; + NodeRef phase_offset; NodeRef sync; BufferRef phase_map; @@ -40,7 +40,7 @@ class Wavetable2D : public Node Wavetable2D(BufferRef2D buffer = nullptr, NodeRef frequency = 440, NodeRef crossfade = 0.0, - NodeRef phase = 0.0, + NodeRef phase_offset = 0.0, NodeRef sync = 0); virtual void alloc() override; @@ -50,10 +50,10 @@ class Wavetable2D : public Node BufferRef2D buffer; NodeRef frequency; NodeRef crossfade; - NodeRef phase; + NodeRef phase_offset; NodeRef sync; - std::vector phase_offset; + std::vector current_phase; }; REGISTER(Wavetable, "wavetable") diff --git a/source/src/node/oscillators/saw.cpp b/source/src/node/oscillators/saw.cpp index 4f9c5a4d..4449b019 100644 --- a/source/src/node/oscillators/saw.cpp +++ b/source/src/node/oscillators/saw.cpp @@ -4,21 +4,21 @@ namespace signalflow { -SawOscillator::SawOscillator(NodeRef frequency, NodeRef phase, NodeRef reset) - : frequency(frequency), phase(phase), reset(reset) +SawOscillator::SawOscillator(NodeRef frequency, NodeRef phase_offset, NodeRef reset) + : frequency(frequency), phase_offset(phase_offset), reset(reset) { SIGNALFLOW_CHECK_GRAPH(); this->name = "saw"; this->create_input("frequency", this->frequency); - this->create_input("phase", this->phase); + this->create_input("phase_offset", this->phase_offset); this->create_input("reset", this->reset); this->alloc(); } void SawOscillator::alloc() { - this->phase_offset.resize(this->num_output_channels_allocated); + this->phase.resize(this->num_output_channels_allocated); } void SawOscillator::trigger(std::string name, float value) @@ -27,7 +27,7 @@ void SawOscillator::trigger(std::string name, float value) { for (int channel = 0; channel < this->num_output_channels; channel++) { - this->phase_offset[channel] = 0; + this->phase[channel] = 0; } } } @@ -43,26 +43,26 @@ void SawOscillator::process(Buffer &out, int num_frames) { if (this->reset->out[channel][frame]) { - this->phase_offset[channel] = 0; + this->phase[channel] = 0; } } - if (this->phase) + if (this->phase_offset) { - phase_cur = fmodf(this->phase_offset[channel] + this->phase->out[channel][frame], 1.0); + phase_cur = fmodf(this->phase[channel] + this->phase_offset->out[channel][frame], 1.0); } else { - phase_cur = this->phase_offset[channel]; + phase_cur = this->phase[channel]; } float rv = (phase_cur * 2.0) - 1.0; out[channel][frame] = rv; - this->phase_offset[channel] += this->frequency->out[channel][frame] / this->graph->get_sample_rate(); - while (this->phase_offset[channel] >= 1.0) + this->phase[channel] += this->frequency->out[channel][frame] / this->graph->get_sample_rate(); + while (this->phase[channel] >= 1.0) { - this->phase_offset[channel] -= 1.0; + this->phase[channel] -= 1.0; } } } diff --git a/source/src/node/oscillators/sine.cpp b/source/src/node/oscillators/sine.cpp index ddbcbe7d..72aa5925 100644 --- a/source/src/node/oscillators/sine.cpp +++ b/source/src/node/oscillators/sine.cpp @@ -4,13 +4,15 @@ namespace signalflow { -SineOscillator::SineOscillator(NodeRef frequency) - : frequency(frequency) +SineOscillator::SineOscillator(NodeRef frequency, NodeRef phase_offset, NodeRef reset) + : frequency(frequency), phase_offset(phase_offset), reset(reset) { SIGNALFLOW_CHECK_GRAPH(); this->name = "sine"; this->create_input("frequency", this->frequency); + this->create_input("phase_offset", this->phase_offset); + this->create_input("reset", this->reset); /*-------------------------------------------------------------------------------- * This can't be done in Node::Node because base class constructors @@ -35,17 +37,50 @@ void SineOscillator::alloc() void SineOscillator::process(Buffer &out, int num_frames) { + /*-------------------------------------------------------------------------------- + * Precalculate phase increment for efficiency + *--------------------------------------------------------------------------------*/ + float phase_increment_scale = M_PI * 2.0 / this->graph->get_sample_rate(); + for (int channel = 0; channel < this->num_output_channels; channel++) { for (int frame = 0; frame < num_frames; frame++) { - float freq = this->frequency->out[channel][frame]; - out[channel][frame] = sin(this->phase[channel] * M_PI * 2.0); - this->phase[channel] += freq / this->graph->get_sample_rate(); + if (SIGNALFLOW_CHECK_TRIGGER(this->reset, frame)) + { + this->phase[channel] = 0; + } + + out[channel][frame] = this->phase[channel]; + + this->phase[channel] += this->frequency->out[channel][frame] * phase_increment_scale; + + /*-------------------------------------------------------------------------------- + * This formulation is much more efficient than fmod(). + * Performing a single modulo after the for(frame) loop was trialled, but + * resulted in rounding errors and aliasing in the corresponding output frequency + * even when using a double for phase - not sure why. + *--------------------------------------------------------------------------------*/ + while (this->phase[channel] > M_PI * 2) + { + this->phase[channel] -= M_PI * 2; + } + } - while (this->phase[channel] > 1.0) - this->phase[channel] -= 1.0; +#ifdef __APPLE__ + if (this->phase_offset) + { + vDSP_vadd(out[channel], 1, this->phase_offset->out[channel], 1, out[channel], 1, num_frames); + } + + vvsinf(out[channel], out[channel], &num_frames); +#else + for (int frame = 0; frame < num_frames; frame++) + { + out[channel][frame] += this->phase_offset->out[channel][frame]; + out[channel][frame] = sinf(out[channel][frame]); } +#endif } } diff --git a/source/src/node/oscillators/wavetable.cpp b/source/src/node/oscillators/wavetable.cpp index d6b9f903..6ace4ab4 100644 --- a/source/src/node/oscillators/wavetable.cpp +++ b/source/src/node/oscillators/wavetable.cpp @@ -4,15 +4,15 @@ namespace signalflow { -Wavetable::Wavetable(BufferRef buffer, NodeRef frequency, NodeRef phase, NodeRef sync, BufferRef phase_map) - : buffer(buffer), frequency(frequency), phase(phase), sync(sync), phase_map(phase_map) +Wavetable::Wavetable(BufferRef buffer, NodeRef frequency, NodeRef phase_offset, NodeRef sync, BufferRef phase_map) + : buffer(buffer), frequency(frequency), phase_offset(phase_offset), sync(sync), phase_map(phase_map) { SIGNALFLOW_CHECK_GRAPH(); this->name = "wavetable"; this->create_input("frequency", this->frequency); - this->create_input("phase", this->phase); + this->create_input("phase_offset", this->phase_offset); this->create_input("sync", this->sync); this->create_buffer("buffer", this->buffer); this->create_buffer("phase_map", this->phase_map); @@ -45,7 +45,7 @@ void Wavetable::process(Buffer &out, int num_frames) float frequency = this->frequency->out[channel][frame]; // TODO Create wavetable buffer - float index = this->current_phase[channel] + this->phase->out[channel][frame]; + float index = this->current_phase[channel] + this->phase_offset->out[channel][frame]; index = fmod(index, 1); while (index < 0) { @@ -67,14 +67,14 @@ void Wavetable::process(Buffer &out, int num_frames) } } -Wavetable2D::Wavetable2D(BufferRef2D buffer, NodeRef frequency, NodeRef crossfade, NodeRef phase, NodeRef sync) - : buffer(buffer), frequency(frequency), crossfade(crossfade), phase(phase), sync(sync) +Wavetable2D::Wavetable2D(BufferRef2D buffer, NodeRef frequency, NodeRef crossfade, NodeRef phase_offset, NodeRef sync) + : buffer(buffer), frequency(frequency), crossfade(crossfade), phase_offset(phase_offset), sync(sync) { this->name = "wavetable2d"; this->create_input("frequency", this->frequency); this->create_input("crossfade", this->crossfade); - this->create_input("phase", this->phase); + this->create_input("phase_offset", this->phase_offset); this->create_input("sync", this->sync); // Named Buffer inputs don't yet work for Buffer2Ds :-( @@ -85,7 +85,7 @@ Wavetable2D::Wavetable2D(BufferRef2D buffer, NodeRef frequency, NodeRef crossfad void Wavetable2D::alloc() { - this->phase_offset.resize(this->num_output_channels_allocated); + this->current_phase.resize(this->num_output_channels_allocated); } void Wavetable2D::process(Buffer &out, int num_frames) @@ -96,7 +96,7 @@ void Wavetable2D::process(Buffer &out, int num_frames) { float frequency = this->frequency->out[channel][frame]; - float current_phase = this->phase_offset[channel] + this->phase->out[channel][frame]; + float current_phase = this->current_phase[channel] + this->phase_offset->out[channel][frame]; current_phase = fmod(current_phase, 1); while (current_phase < 0) { @@ -108,9 +108,9 @@ void Wavetable2D::process(Buffer &out, int num_frames) out[channel][frame] = rv; - this->phase_offset[channel] += (frequency / this->graph->get_sample_rate()); - while (this->phase_offset[channel] >= 1.0) - this->phase_offset[channel] -= 1.0; + this->current_phase[channel] += (frequency / this->graph->get_sample_rate()); + while (this->current_phase[channel] >= 1.0) + this->current_phase[channel] -= 1.0; } } } diff --git a/source/src/python/nodes.cpp b/source/src/python/nodes.cpp index 02d306d9..6c39fc39 100644 --- a/source/src/python/nodes.cpp +++ b/source/src/python/nodes.cpp @@ -260,13 +260,13 @@ void init_python_nodes(py::module &m) .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0); py::class_>(m, "SawOscillator", "Produces a (non-band-limited) sawtooth wave, with the given `frequency` and `phase` offset. When a `reset` or trigger is received, resets the phase to zero.") - .def(py::init(), "frequency"_a = 440, "phase"_a = nullptr, "reset"_a = nullptr); + .def(py::init(), "frequency"_a = 440, "phase_offset"_a = nullptr, "reset"_a = nullptr); py::class_>(m, "SineLFO", "Produces a sinusoidal LFO at the given `frequency` and `phase` offset, with output ranging from `min` to `max`.") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0); py::class_>(m, "SineOscillator", "Produces a sine wave at the given `frequency`.") - .def(py::init(), "frequency"_a = 440); + .def(py::init(), "frequency"_a = 440, "phase_offset"_a = nullptr, "reset"_a = nullptr); py::class_>(m, "SquareLFO", "Produces a pulse wave LFO with the given `frequency` and pulse `width`, ranging from `min` to `max`, where `width` of `0.5` is a square wave and other values produce a rectangular wave.") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "width"_a = 0.5, "phase"_a = 0.0); @@ -281,10 +281,10 @@ void init_python_nodes(py::module &m) .def(py::init(), "frequency"_a = 440); py::class_>(m, "Wavetable", "Plays the wavetable stored in buffer at the given `frequency` and `phase` offset. `sync` can be used to provide a hard sync input, which resets the wavetable's phase at each zero-crossing.") - .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "phase"_a = 0, "sync"_a = 0, "phase_map"_a = nullptr); + .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "phase_offset"_a = 0, "sync"_a = 0, "phase_map"_a = nullptr); py::class_>(m, "Wavetable2D", "Wavetable2D") - .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "crossfade"_a = 0.0, "phase"_a = 0.0, "sync"_a = 0); + .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "crossfade"_a = 0.0, "phase_offset"_a = 0.0, "sync"_a = 0); py::class_>(m, "Clip", "Clip the input to `min`/`max`.") .def(py::init(), "input"_a = nullptr, "min"_a = -1.0, "max"_a = 1.0); diff --git a/tests/test_nodes_oscillators.py b/tests/test_nodes_oscillators.py index 7bbd74f9..96536834 100644 --- a/tests/test_nodes_oscillators.py +++ b/tests/test_nodes_oscillators.py @@ -7,7 +7,6 @@ Fs = 1000 N = 1024 - def test_nodes_oscillators_sine(graph): a = sf.SineOscillator([10, 20]) graph.render_subgraph(a) @@ -23,6 +22,18 @@ def test_nodes_oscillators_sine(graph): expected = np.sin(np.arange(graph.output_buffer_size) * np.pi * 2 * 20 / graph.sample_rate) assert a.output_buffer[1] == pytest.approx(expected, abs=0.0001) + # -------------------------------------------------------------------------------- + # Phase shift + # -------------------------------------------------------------------------------- + a = sf.SineOscillator([10, 20], [np.pi * 2 * 0.25, np.pi * 2 * 0.5]) + graph.render_subgraph(a) + + expected = np.sin(np.arange(graph.output_buffer_size) * np.pi * 2 * 10 / graph.sample_rate + (np.pi * 2 * 0.25)) + assert a.output_buffer[0] == pytest.approx(expected, abs=0.0001) + + expected = np.sin(np.arange(graph.output_buffer_size) * np.pi * 2 * 20 / graph.sample_rate + (np.pi * 2 * 0.5)) + assert a.output_buffer[1] == pytest.approx(expected, abs=0.0001) + def test_nodes_oscillators_saw(graph): a = sf.SawOscillator([1, 2])