Skip to content

Commit

Permalink
Add vDSP vectorisation to SineOscillator, reducing processing time by…
Browse files Browse the repository at this point in the history
… 50%; add and test phase_offset
  • Loading branch information
ideoforms committed Feb 18, 2024
1 parent 586b4a4 commit 1d3a0a7
Show file tree
Hide file tree
Showing 12 changed files with 97 additions and 49 deletions.
2 changes: 1 addition & 1 deletion docs/library/oscillators/sawoscillator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/library/oscillators/sineoscillator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion docs/library/oscillators/wavetable/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/library/oscillators/wavetable2d/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions source/include/signalflow/node/oscillators/saw.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<float> phase_offset;
std::vector<float> phase;
};

REGISTER(SawOscillator, "saw")
Expand Down
4 changes: 3 additions & 1 deletion source/include/signalflow/node/oscillators/sine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<float> phase;
Expand Down
10 changes: 5 additions & 5 deletions source/include/signalflow/node/oscillators/wavetable.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -26,7 +26,7 @@ class Wavetable : public Node
private:
BufferRef buffer;
NodeRef frequency;
NodeRef phase;
NodeRef phase_offset;
NodeRef sync;
BufferRef phase_map;

Expand All @@ -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;
Expand All @@ -50,10 +50,10 @@ class Wavetable2D : public Node
BufferRef2D buffer;
NodeRef frequency;
NodeRef crossfade;
NodeRef phase;
NodeRef phase_offset;
NodeRef sync;

std::vector<float> phase_offset;
std::vector<float> current_phase;
};

REGISTER(Wavetable, "wavetable")
Expand Down
24 changes: 12 additions & 12 deletions source/src/node/oscillators/saw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
}
}
Expand All @@ -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;
}
}
}
Expand Down
49 changes: 42 additions & 7 deletions source/src/node/oscillators/sine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down
24 changes: 12 additions & 12 deletions source/src/node/oscillators/wavetable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand All @@ -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 :-(
Expand All @@ -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)
Expand All @@ -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)
{
Expand All @@ -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;
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions source/src/python/nodes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,13 @@ void init_python_nodes(py::module &m)
.def(py::init<NodeRef, NodeRef, NodeRef, NodeRef>(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0);

py::class_<SawOscillator, Node, NodeRefTemplate<SawOscillator>>(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<NodeRef, NodeRef, NodeRef>(), "frequency"_a = 440, "phase"_a = nullptr, "reset"_a = nullptr);
.def(py::init<NodeRef, NodeRef, NodeRef>(), "frequency"_a = 440, "phase_offset"_a = nullptr, "reset"_a = nullptr);

py::class_<SineLFO, Node, NodeRefTemplate<SineLFO>>(m, "SineLFO", "Produces a sinusoidal LFO at the given `frequency` and `phase` offset, with output ranging from `min` to `max`.")
.def(py::init<NodeRef, NodeRef, NodeRef, NodeRef>(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0);

py::class_<SineOscillator, Node, NodeRefTemplate<SineOscillator>>(m, "SineOscillator", "Produces a sine wave at the given `frequency`.")
.def(py::init<NodeRef>(), "frequency"_a = 440);
.def(py::init<NodeRef, NodeRef, NodeRef>(), "frequency"_a = 440, "phase_offset"_a = nullptr, "reset"_a = nullptr);

py::class_<SquareLFO, Node, NodeRefTemplate<SquareLFO>>(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<NodeRef, NodeRef, NodeRef, NodeRef, NodeRef>(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "width"_a = 0.5, "phase"_a = 0.0);
Expand All @@ -281,10 +281,10 @@ void init_python_nodes(py::module &m)
.def(py::init<NodeRef>(), "frequency"_a = 440);

py::class_<Wavetable, Node, NodeRefTemplate<Wavetable>>(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<BufferRef, NodeRef, NodeRef, NodeRef, BufferRef>(), "buffer"_a = nullptr, "frequency"_a = 440, "phase"_a = 0, "sync"_a = 0, "phase_map"_a = nullptr);
.def(py::init<BufferRef, NodeRef, NodeRef, NodeRef, BufferRef>(), "buffer"_a = nullptr, "frequency"_a = 440, "phase_offset"_a = 0, "sync"_a = 0, "phase_map"_a = nullptr);

py::class_<Wavetable2D, Node, NodeRefTemplate<Wavetable2D>>(m, "Wavetable2D", "Wavetable2D")
.def(py::init<BufferRef2D, NodeRef, NodeRef, NodeRef, NodeRef>(), "buffer"_a = nullptr, "frequency"_a = 440, "crossfade"_a = 0.0, "phase"_a = 0.0, "sync"_a = 0);
.def(py::init<BufferRef2D, NodeRef, NodeRef, NodeRef, NodeRef>(), "buffer"_a = nullptr, "frequency"_a = 440, "crossfade"_a = 0.0, "phase_offset"_a = 0.0, "sync"_a = 0);

py::class_<Clip, Node, NodeRefTemplate<Clip>>(m, "Clip", "Clip the input to `min`/`max`.")
.def(py::init<NodeRef, NodeRef, NodeRef>(), "input"_a = nullptr, "min"_a = -1.0, "max"_a = 1.0);
Expand Down
13 changes: 12 additions & 1 deletion tests/test_nodes_oscillators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
Fs = 1000
N = 1024


def test_nodes_oscillators_sine(graph):
a = sf.SineOscillator([10, 20])
graph.render_subgraph(a)
Expand All @@ -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])
Expand Down

0 comments on commit 1d3a0a7

Please sign in to comment.