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.
In this article, we’ll focus on the sinusoidal LFO, as the other waveforms types are relatively easier to reproduce.
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.
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); }
-
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); }