Animation with Low Frequency Oscillators (LFOs)

Often there are situations working with data streams where you might want to ‘tween’ or animate changing values with a touch of ‘organic’ movement or variance. An example is where you want the brightness of an LED to breathe gently around a preset value.

As the name implies, LFOs are essentially algorithms that transition around a key displacement value 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. It’s sometimes misleading as the oscillation speed might still be quick, by human standards!

What’s the difference?

Compare this with Data Smoothies & Interpolation or the Digital to Simulated Analogue – what are the differences between these three technical concepts?

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

Different types of LFOs. Source: [Square wave, Wikipedia](https://en.wikipedia.org/wiki/Square_wave)

Different types of LFOs. Source: Square wave, Wikipedia

In this article, we’ll focus on the sinusoidal LFO, as the other waveforms types are relatively easier to reproduce.

An example of an LFO, using the sine wave. Source: [Sine Wave, Wikipedia](https://simple.wikipedia.org/wiki/Sine_wave)

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

The term ‘LFO’ is rooted in audio engineering and electronic music – if you have worked with audio, you would have come across this when dabbling in audio signal effect chains. However, LFOs are not just specific to the audio world. They are known in other circles such as animation and motion graphics, but don’t use the LFO moniker.

Note

LFOs are not to be confused with easing curves. LFOs oscillate around a set point, while easing curves interpolate from one point to another, over a predefined curve.


Making sense of the LFO

Referring to previous diagram, 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. If we turn this graph counter-clockwise by 90 degrees, we see the dotted line as vertical, with the red sine curve ‘swaying’ left and right of this reference.

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.

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.

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’. Like all data points, amplitude and frequency can be dynamically modified by other data streams to generate highly organic movement. This can result in non-repeating and ‘natural’ looking animations. On top of this, when the key value changes, the LFO ‘travels’ along with that base key, providing a nice, self-contained behaviour.

In our case, we can implement LFOs to generate organic swells and textures to an otherwise gradually-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.

The code example below 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 with the Digital to Simulated Analogue or Data Smoothies & Interpolation coding concepts. 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.

Pro Tip for Particle Device Users

Use the Arduino IDE’s Serial Plotter as a convenient way of visualising ‘live’ data from a Particle Device such as the Photon. The Serial Plotter is just a rudimentary data visualiser of any Serial port. You will need to connect your Particle Device via the USB cable to view this data stream. Check also the Serial.printlnf() command on how the values are printed and formatted. Third-party utilities such as CoolTerm or RealtimePlotter gives you even better visualisation options.


Going Further

The example code below demonstrates an inefficient method in getting a sine-based LFO to work (i.e. engineers and coders will shake their heads in horror at the sheer computing and memory wastage). Why? Performing trigonometry functions on a microcontroller can be costly, both in terms of computing power and memory. The math.h library does a decent job exposing the trigonometry functions for use, and newer microcontrollers are more than capable of driving the LFOs smoothly.

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.

If it aint’ broke…

Unless you are working on an extreme case scenario, LFO optimisation can sometimes be more of an academic pursuit. Consider the options in this section as options. As creative practitioners our first aim is often just to get it working (safely).

Sine Look-up Tables (LUT) - a computationally efficient alternative method

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.

This Sine Look Up Table Generator Calculator is an online tool where you can pre-generate 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 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:

  • Stick with dynamically-calculated sine tables if you’re just beginning to learn this concept
  • Use dynamically-calculated sine tables to start out, work out the parameters that require dynamic updates
  • Convert to using LUTs if your LFOs are not going to change frequency (i.e. constant speed/wavelength)
  • When using LUTs, set a high enough amplitude so you can perform integer math instead of floating point Math

Pro tip: Even further optimisation

An even more efficient way of generating a pure sine LUT is to define just a 1/4 wave – the other three quarters are simply reflected sequences of the first quarter that you can easily iterate backwards/invert in code!


Code

This article assumes you know how to wire up a servo – check the Expression Recipes for more details and use the tabs below to view code specific to the microcontroller.

For the Arduino UNO examples, use the Serial Monitor to send new incoming values. On the Particle Device, you’ll call a Particle Function from the Particle CLI, or other compatible API calls, to set new values. As always, experiment with the code.

Examples are available for a servo and an 8-pixel NeoPixel strip. They both use the same LFO algorithms and show how the LFO can be used in various expression applications.

  • Libraries Used

    • math.h
      This is a built-in library, you don’t need to install it (but you’ll still need the #include "math.h" statement)
    • Servo
      This is a built-in library, you don’t need to install it (but you’ll still need the #include "Servo.h" statement)

    Code

    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
    
    /*
        LFOs – using low-frequency oscillators on the Arduino UNO with a servo
    */
    /*
        Please note that the code provided here is licensed under the MIT license.
    
        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 "Servo.h"      // likewise, the Servo library
    Servo myservo;
    
    // setting up some variables to manage incoming test Serial data:
    String inputString = "";      // a String to hold incoming data
    bool stringComplete = false;  // whether the string is complete
    
    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;    // start the experiment by pointing the servo to 90
    
    void setup() {
        myservo.attach(3);             // servo attached to pin 3
        myservo.write(servoAngle);
    
        Serial.begin(115200);
        delay(5000);        // Common safety practice to allow board to 'idle' with mechanical components connected
    }
    
    // code in this loop function runs forever, until you cut power!
    void loop() {
        checkSerial();             // keep checking the Serial port for available data
        updateSine();
        updateServo();
    
        // add a tiny delay so as not to overwhelm the servo!
        // do NOT comment this line out as you will stress out the servo timing!
        delay(5);
    }
    
    //////////// CUSTOM FUNCTIONS BELOW
    
    // checkSerial runs each time loop() runs, to check for incoming data from our Serial Monitor
    void checkSerial() {
      if (Serial.available() > 0) {
        // get the new byte:
        char inChar = (char)Serial.read();
        // add it to the inputString:
        inputString += inChar;
        // if the incoming character is a newline, set a flag so the main loop can
        // do something about it:
        if (inChar == '\n') {
          stringComplete = true;
        }
      }
    
      if (stringComplete) {
        // let's convert the incoming string into a number
        keyVal = inputString.toInt();
        Serial.print(">>> keyVal is now: ");
        Serial.println(keyVal);
        inputString = "";  // clear the string
        stringComplete = false;
      }
    }
    
    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);
        // we print these out in this sequence so that the
        // Serial Plotter will have a consistent 'floor' and
        // 'ceiling' of 0 and 180 respectively, resulting in
        // a graph that won't auto-scale the y-axis
        Serial.print("0,180,");
        Serial.print(keyVal);
        Serial.println(servoAngle);
    }
    
  • Libraries Used

    • math.h
      This is a built-in library, you don’t need to install it (but you’ll still need the #include "math.h" statement)
    • Adafruit_NeoPixel
      You’ll need to install this from the Library Manager

    Code

    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
    
    /*
      LFOs – using low-frequency oscillators on the Arduino UNO with NeoPixels
    */
    /*
        Please note that the code provided here is licensed under the MIT license.
    
        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 <Adafruit_NeoPixel.h>   // likewise, the NeoPixel library
    
    // IMPORTANT: Set pixel COUNT, PIN and TYPE
    #define PIXEL_PIN 3
    #define PIXEL_COUNT 8
    #define PIXEL_TYPE WS2812B
    
    Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);
    
    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(115200);
    
        pinMode(PIXEL_PIN, 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
        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;
    }
    
    /////////////////////////// CUSTOM FUNCTIONS
    // 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);
    }
    
  • Sample flows showing 3 Particle Functions used to change the LFO parameters dynamically:<br/>**newKey** sets the key displacement value from which the LFO oscillates around;<br/>**newAmp** sets the amplitude;<br/>**newRes** sets the resolution of the curve (higher = slower).

    Sample flows showing 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).

    Libraries Used

    • math.h
      This is a built-in library, you don’t need to install it (but you’ll still need the #include "math.h" statement)
    • Servo
      This is a built-in library, you don’t need to install it (but you’ll still need the #include "Servo.h" statement)

    Code

    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
    
    /*
        LFOs – using low-frequency oscillators on a Particle Device with a servo
    */
    /*
        Please note that the code provided here is licensed under the MIT license.
    
        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 "Servo.h"      // likewise, the Servo library
    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;    // start the experiment by pointing the servo to 90
    
    void setup() {
        myservo.attach(D0);
        myservo.write(servoAngle);
    
        Serial.begin(115200);
    
        // set up the Particle Functions
        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!
        // do NOT comment out as you will stress out the servo timing
        // which may destroy your Particle Device'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);
        // we print these out in this sequence so that the
        // Serial Plotter will have a consistent 'floor' and
        // 'ceiling' of 0 and 180 respectively, resulting in
        // a graph that won't auto-scale the y-axis
        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;    
    }
    
  • The following Particle Functions are made available for customisation:

    • newKey – set the new key value (integer)
    • newAmp – set the new amplitude (integer)
    • newRes – set the resolution/frequency of the sine curve increment; larger = higher resolution = slower (integer)
    • newRGB – set a new RGB colour (e.g. “0,255,0” for green)

    Libraries Used

    • math.h
      This is a built-in library, you don’t need to install it (but you’ll still need the #include "math.h" statement)
    • Adafruit_NeoPixel
      You’ll need to install this from the Library Manager

    Code

    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
    
    /*
      LFOs – using low-frequency oscillators on the Particle Device with NeoPixels
    */
    /*
        Please note that the code provided here is licensed under the MIT license.
    
        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 <Adafruit_NeoPixel.h>   // likewise, the NeoPixel library
    
    // IMPORTANT: Set pixel COUNT, PIN and TYPE
    #define PIXEL_PIN D0
    #define PIXEL_COUNT 8
    #define PIXEL_TYPE WS2812B
    
    Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);
    
    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(115200);
    
        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();
    
        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;
    }
    
    /////////////////////////// CUSTOM FUNCTIONS
    // 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);
    }
    

This page was last updated: 23 Sep 2024