Sinusoidal LFOs

LFOs stand for Low Frequency Oscillators. There are many different types of LFOs, the most common of which are triangle, square, sawtooth and sinusoidal.

Different types of LFOs. Source: Square wave, Wikipedia

In this recipe we’ll focus on the sinusoidal LFO.

As the name implies, LFOs are essentially algorithms that transition around a key displacement incrementally over time. The ‘low’ in LFO refers to the relatively slow and predictable rate at which the transitions occur, generally at a speed slow enough for human perception.

An example of an LFO, using the sine wave. Source: Sine Wave, Wikipedia

The term ‘LFO’ is rooted in audio engineering and electronic music – those of you who work in audio would have come across this when working with audio signal effect chains, and/or amplifiers. However LFOs are not just specific to the audio world. They are known in other circles such as animation and motion graphics, too.


Making sense of the LFO

Referring to the graph above, visualise the dotted line as the ‘key’ value – for example, this could be the incoming data stream that you are receiving from a sensor feed, or some other data source. The red sine curve is the undulating ‘texture’ that oscillates around this ‘key’ value. This means that the final reading will be offset by this oscillation.

Both the amplitude and frequency of the sine wave can be further manipulated, which gives you an opportunity to create an even more organic ‘texture’. And like all data points, amplitude and frequency can be dynamically modified by other data streams to generate highly organic movement that can be potentially non-repeating and ‘natural’ looking. On top of this, when the key value changes, the LFO ‘travels’ along with that base key, giving you a nice, self-contained algorithm that is highly tune-able to your needs.

In our case, we can implement LFOs to generate organic swells and textures to an otherwise slow-changing data stream. This makes your expressions far more natural, as they will continue to have basic movement even when no new data is coming in.

Sample output of this recipe, visualised using the Arduino IDE’s Serial Plotter. The keyValue is set at 90, with the amplitude also set to 90, hence achieving a full 0-180 sweep that will make the servo connected at pin D0 do a full sinusoidal sweep. These values, along with the frequency (i.e. wavelength), can be easily set in IoTa through the exposed Particle Functions (see code below).

This recipe presents a DIY solution that does not rely on 3rd-party libraries, and instead utilises a simple sine wave algorithm to demonstrate how the concept works. You might find it really useful to combine this recipe with Sensor Ramp or Data Smoothies. Once you figure out how to integrate the two, you can then graduate towards applying custom easing curves using formulae beyond the simple sine wave function.

Sample flows showing the 3 Particle Functions used to change the LFO parameters dynamically.
newKey sets the key displacement value from which the LFO oscillates around;
newAmp sets the amplitude;
newRes sets the resolution of the curve (higher = slower)

Going Further

This recipe shows probably the ‘slowest’ method in getting a sine-based LFO to work (i.e. engineers and coders will shake their heads in horror at the sheer wastage and excess). Performing trigonometry functions on a microcontroller can be costly, both in terms of computing power and memory.

If you refer to the code below, the double datatype is a double-precision float which requires 64 bits per variable declared. That’s a whopping 8 bytes for just one variable. This adds up pretty quickly on a microcontroller, particularly if you intend to run multiple LFOs at the same time.

Unless you are working on an extreme case scenario, optimisation can sometimes be more of an academic pursuit. The math.h library does a decent job of the trig functions, and as you can see in this recipe, it is more than capable of driving the LFOs smoothly.

Sine Look-up Tables (LUT)

Look-up tables are an alternative to generating sine values on the fly. The idea is to have a pre-generated list of sine values that you expect to use at a particular amplitude and frequency. Using this ‘look-up table’, all you need to do is to iterate over the values in this list to get the smooth readouts expected from a sine curve.

http://www.daycounter.com/Calculators/Sine-Generator-Calculator.phtml is a generator where you can pre-set the resolution and amplitude of your sine curve.

Because this array is pre-generated, the only calculations needed are simpler arithmetic to scale the amplitude to a desired range. The benefit here is if you generate large integer values for the amplitude, floating point (decimal) calculations are completely avoided which gives much faster performance. The drawback to this method, however, is the resolution – you can’t generate another look-up table with finer or coarser resolution easily, and will have to manage be limited to coarser multiples of the original resolution which might not provide a clean transition between the start and end of the sine curves when you use non-even multipliers.

Long story short:

https://en.wikipedia.org/wiki/CORDIC


Libraries Used

(learn how to import them in the Build IDE):


Code variant 1: with a servo

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/*
	LFOs – using low-frequency oscillators on the Photon with a servo

	The MIT License (MIT)
	Copyright © 2018 Chuan Khoo

	Permission is hereby granted, free of charge, to any person obtaining
	a copy of this software and associated documentation files (the
	"Software"), to deal in the Software without restriction, including
	without limitation the rights to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to
	permit persons to whom the Software is furnished to do so, subject to
	the following conditions:

	The above copyright notice and this permission notice shall be included
	in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
	THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
	OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
	ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
	OTHER DEALINGS IN THE SOFTWARE.
*/
#include "math.h"       // include the Math library because we will be using sine calculations

Servo myservo;

const double TWOPI = 2*M_PI;

int keyVal = 0;         // the baseline 'key' from which the LFO oscillates around
int LFOVal = 0;         // the offset LFO with amplitude applied. keyVal + LFOVal = resulting final y-position
int sineRes = 1000;     // set the resolution/frequency of the sine curve increment; larger = higher resolution = slower
int sineAmp = 100;      // sets the amplitude scale

// determine how large each 'step' is when traversing through the sine curve. 2*PI because x=2*PI for a complete sine curve
double sineIncre = TWOPI/sineRes;
double sineCnt = 0.0f;  // variable to hold the current 'step' position along the x-axis of the sine curve
double sineVal = 0.0f;  // variable to hold the calculated sine value

int servoAngle = 90;

void setup() {
    myservo.attach(D0);
    myservo.write(servoAngle);

    Serial.begin(57600);

    Particle.function("newKey", newKey);
    Particle.function("newAmp", newAmp);
    Particle.function("newRes", newRes);

    delay(5000);        // Common practice to allow board to 'settle' after connecting online
}

// code in this loop function runs forever, until you cut power!
void loop() {
    updateSine();
    updateServo();

    // add a tiny delay so as not to overwhelm the servo / Photon!
    // do NOT comment out as you will stress out the servo timing
    // which may destroy your Photon's voltage regulator!
    delay(5);
}

void updateSine() {
    if(sineCnt >= TWOPI) {
        sineCnt = 0.0f;
    } else {
        sineCnt += sineIncre;
        sineVal = sin(sineCnt);
        LFOVal = round(sineVal*sineAmp);
    }
}

void updateServo() {
    int final = keyVal + LFOVal;
    servoAngle = constrain(final, 0, 180);   // sanity check; keep everything within servo range

    myservo.write(servoAngle);
    Serial.printlnf("%i,%i,%i,%i", 0, 180, keyVal, servoAngle);
}


///////// PARTICLE FUNCTIONS

int newKey(String command) {
    int k = atoi(command);

    if(k != NULL) {
        keyVal = k;
    } else {
        return -1;
    }
    return 1;    
}

int newAmp(String command) {
    int a = atoi(command);

    if(a != NULL) {
        sineAmp = a;
    } else {
        return -2;
    }
    return 2;    
}

int newRes(String command) {
    int r = atoi(command);

    if(r != NULL) {
        sineRes = r;
        sineIncre = TWOPI/sineRes;
    } else {
        return -3;
    }
    return 3;    
}

Code variant 2: with 8 NeoPixels

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*
	LFOs – using low-frequency oscillators on the Photon with NeoPixels

	The MIT License (MIT)
	Copyright © 2018 Chuan Khoo

	Permission is hereby granted, free of charge, to any person obtaining
	a copy of this software and associated documentation files (the
	"Software"), to deal in the Software without restriction, including
	without limitation the rights to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to
	permit persons to whom the Software is furnished to do so, subject to
	the following conditions:

	The above copyright notice and this permission notice shall be included
	in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
	THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
	OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
	ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
	OTHER DEALINGS IN THE SOFTWARE.
*/
#include "math.h"       // include the Math library because we will be using sine calculations
#include <neopixel.h>

// IMPORTANT: Set pixel COUNT, PIN and TYPE
#define PIXEL_PIN D0
#define PIXEL_COUNT 8
#define PIXEL_TYPE WS2812B

Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);

int newIoTaValue = 0;

const double TWOPI = 2*M_PI;

int keyVal = 0;         // the baseline 'key' from which the LFO oscillates around
int LFOVal = 0;         // the offset LFO with amplitude applied. keyVal + LFOVal = resulting final y-position
int sineRes = 1000;     // set the resolution/frequency of the sine curve increment; larger = higher resolution = slower
int sineAmp = 100;      // sets the amplitude scale

int r = 0;
int g = 0;
int b = 0;

// determine how large each 'step' is when traversing through the sine curve. 2*PI because x=2*PI for a complete sine curve
double sineIncre = TWOPI/sineRes;
double sineCnt = 0.0f;  // variable to hold the current 'step' position along the x-axis of the sine curve
double sineVal = 0.0f;  // variable to hold the calculated sine value


void setup() {
    Serial.begin(57600);

    pinMode(D0, OUTPUT);            // goes to Neopixel stick

    strip.begin();
    strip.show(); // Initialize all pixels to 'off'

    Particle.function("newKey", newKey);
    Particle.function("newAmp", newAmp);
    Particle.function("newRes", newRes);

    Particle.function("newRGB", newRGB);

    delay(5000);        // Common practice to allow board to 'settle' after connecting online
}

// code in this loop function runs forever, until you cut power!
void loop() {
    updateSine();
    updateNeoPixels();

    // add a tiny delay so as not to overwhelm the servo / Photon!
    // do NOT comment out as you will stress out the servo timing
    // which may destroy your Photon's voltage regulator!
    delay(5);
}

void updateSine() {
    if(sineCnt >= TWOPI) {
        sineCnt = 0.0f;
    } else {
        sineCnt += sineIncre;
        sineVal = sin(sineCnt);
        LFOVal = round(sineVal*sineAmp);
    }
}

void updateNeoPixels() {
    int final = keyVal + LFOVal;
    newIoTaValue = constrain(final, 0, 255);   // sanity check; keep everything within servo range

    int lfoR = (newIoTaValue/255.0) * r;
    int lfoG = (newIoTaValue/255.0) * g;
    int lfoB = (newIoTaValue/255.0) * b;

    colorAll(strip.Color(lfoR, lfoG, lfoB), 1);

    Serial.printlnf("%i,%i,%i,%i", 0, 255, keyVal, newIoTaValue);
}


///////// PARTICLE FUNCTIONS

int newKey(String command) {
    int k = atoi(command);

    if(k != NULL) {
        keyVal = k;
    } else {
        return -1;
    }
    return 1;    
}

int newAmp(String command) {
    int a = atoi(command);

    if(a != NULL) {
        sineAmp = a;
    } else {
        return -2;
    }
    return 2;    
}

int newRes(String command) {
    int r = atoi(command);

    if(r != NULL) {
        sineRes = r;
        sineIncre = TWOPI/sineRes;
    } else {
        return -3;
    }
    return 3;    
}

int newRGB(String command) {
    uint8_t res = 1;
    char * params = new char[command.length() + 1];

    strcpy(params, command.c_str());
    char * param1 = strtok(params, ",");    // RED
    char * param2 = strtok(NULL, ",");      // GREEN
    char * param3 = strtok(NULL, ",");      // BLUE

    r = atoi(param1);
    g = atoi(param2);
    b = atoi(param3);

    return res;
}



///////////////////////////

// Set all pixels in the strip to a solid color, then wait (ms)
void colorAll(uint32_t c, uint8_t wait) {
  uint16_t i;

  for(i=0; i<strip.numPixels(); i++) {
    strip.setPixelColor(i, c);
  }
  strip.show();
  delay(wait);
}

// Fill the dots one after the other with a color, wait (ms) after each one
void colorWipe(uint32_t c, uint8_t wait) {
  for(uint16_t i=0; i<strip.numPixels(); i++) {
    strip.setPixelColor(i, c);
    strip.show();
    delay(wait);
  }
}


void rainbow(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256; j++) {
    for(i=0; i<strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel((i+j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  if(WheelPos < 85) {
   return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  } else if(WheelPos < 170) {
   WheelPos -= 85;
   return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  } else {
   WheelPos -= 170;
   return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
}