Generating Waves
In this section, we will explore waves (also called oscillators), essential tools for creating dynamic and expressive media. Oscillators generate repeating waveforms, which can control various outputs such as LEDs or motors. We will also learn how to visualize signals and shape different kinds of waveforms. We will then introduce combining different waves together, either by adding them or through modulation. Finally, we will look at how to use randomness to generate noisy waveforms that feel more natural.
Note
To follow along with the examples, set up a simple circuit:
A potentiometer connected to
A0
to control proprties dynamically.A button connected to pin
2
with an internal pull-up resistor to trigger actions.An LED connected to pin
9
(PWM capable) through a 330 \(\Omega\) resistor.
Visualizing Waves with the Serial Plotter
In this section, we will use serial communication to send data from our Arduino board to our
PC so as to visualize the waves in real time. The print()
and println()
functions allow
you to send data to the serial, which is invaluable for debugging and visualizing data. They will
provide a way to graphically observe how wave properties like amplitude, phase, or frequency affect
the output.
Single Signal
To visualize the data, open the Serial Plotter in the Arduino IDE. The Serial Plotter can graphically display waveforms by interpreting each printed value as a separate line on the graph, making it an invaluable tool to visualize signals such as sensor values and waveforms.
Example: Print the value of the potentiometer:
#include <Plaquette.h>
AnalogIn pot(A0); // The potentiometer
void begin() {}
void step() {
println(pot); // Print the potentiometer value and ends the line
}
Multiple Signals
For multiple waveforms, print their values separated by spaces in a single line, followed by a
newline using println()
.
Example: Print the value of the potentiometer and a sine wave:
#include <Plaquette.h>
AnalogIn pot(A0); // Potentiometer input
SineWave wave(2.0); // Sine wave with period of 2 seconds
void begin() {}
void step() {
print(wave); // Print wave value
print(" "); // Print white space
println(pot); // Print the potentiometer value and ends the line
}
Types of Waves
Plaquette provides 3 types of waves:
SquareWave: Alternates between two levels with sharp transitions. Useful for creating rhythmic on-off patterns such as blinking LEDs or simple tone generators for buzzers. Possesses some properties of digital units.
TriangleWave: Smoothly transitions between two levels in a linear fashion. By varying the width of the wave, you can create a sawtooth wave (width = 0) or an inverted sawtooth wave (width = 1). This is ideal for simulating ramping motions or gradual changes in brightness.
SineWave: Produces a sinusoidal waveform for smoother modulation. Commonly used for creating natural, flowing transitions, such as smooth dimming or speed control.
You can visualize these waves on the Serial Plotter by streaming their values.
Example: Display different waves for comparison:
#include <Plaquette.h>
// Three wave types.
SquareWave square(1.0);
TriangleWave triangle(1.0);
SineWave sine(1.0);
void begin() {}
void step() {
// Print all wave values separated by spaces
print(square); print(" ");
print(triangle); print(" ");
println(sine);
}
Wave Properties
Oscillators are defined by their phase, period, frequency, amplitude, and width. Let us explore these properties and their corresponding functions:
phase(): Sets the initial point in the wave cycle (in range [0, 1]).
period(): Sets the duration of one cycle in seconds.
frequency(): Inverse of period; sets the cycles per second (Hz).
bpm(): Alternative way to set the frequency using beats per minute (BPM).
amplitude(): Sets the peak level of the wave (as % of max) (in range [0, 1]);
width(): Controls the balance between the rising and falling portions of the wave cycle (in range [0, 1]). For each wave type, this property has a specific effect:
For SquareWave, it adjusts the duty cycle (the ratio of ON to OFF time).
For TriangleWave, it determines whether the wave skews towards a sawtooth (width = 0) or inverted sawtooth (width = 1).
For SineWave, it shifts the inflection points of the wave, altering its symmetry.
Initializing Properties
There properties can be initialized in the begin()
to build a specific waveform.
Example: Assign some properties of a wave at program startup:
#include <Plaquette.h>
TriangleWave wave;
void begin() {
wave.frequency(2); // 2 Hz
wave.width(0.9); // width 90%
wave.phase(0.1); // dephased by 10% of period
wave.amplitude(0.5); // 50% amplitude
}
void step() {
println(wave); // Print wave value
}
Changing Properties During Runtime
Properties can also be changed in real-time in the step()
function to create interactive or
evolutive effects.
Example: Control the width of the waves using the potentiometer:
#include <Plaquette.h>
AnalogIn pot(A0); // Potentiometer input
SquareWave square(1.0);
TriangleWave triangle(1.0);
SineWave sine(1.0);
void begin() {}
void step() {
// Assign new width value.
square.width(pot);
triangle.width(pot);
sine.width(pot);
// Print all wave values separated by spaces
print(square); print(" ");
print(triangle); print(" ");
println(sine);
}
Example: Control the period of the waves using the potentiometer. Necessitates remapping potentiometer value to appropriate ranges.
#include <Plaquette.h>
AnalogIn pot(A0); // Potentiometer input
SquareWave square(1.0);
TriangleWave triangle(1.0);
SineWave sine(1.0);
void begin() {}
void step() {
// Read new period value.
float newPeriod = pot.mapTo(0.5, 5); // Map to 0.5-5 seconds period
// Assign new period value.
square.period(newPeriod);
triangle.period(newPeriod);
sine.period(newPeriod);
// Print all wave values separated by spaces
print(square); print(" ");
print(triangle); print(" ");
println(sine);
}
Try using the potentiometer to control different wave properties and visualize the result using the Serial Plotter.
Accessors and Mutators
All properties in wave units have two variants:
A mutator variant allowing to change the value of the property. Example:
wave.period(3.0);
.An accessor read-only variant that returns the current value of the property. Example:
float x = wave.period();
Tip
This naming convention is a standard in Plaquette and you will find it in other units as well.
Example: Increase the wave’s period by one second each time the button is pressed:
#include <Plaquette.h>
DigitalIn button(2, INTERNAL_PULLUP); // Button input
TriangleWave wave(1.0); // Wave with initial 1 second period
void begin() {}
void step() {
if (button.rose()) {
wave.period( wave.period() + 1 ); // Set period to current period plus one
}
println(wave); // Print wave value
}
Wave Addition
Adding waves together allows for the creation of complex and dynamic waveforms. By superimposing multiple signals, you can simulate natural phenomena, generate rhythmic patterns, or create rich textures for artistic applications. In Plaquette, wave addition is as simple as computing the average value of different waves.
One compelling example of wave addition is simulating a heartbeat. A heartbeat typically has two peaks: a stronger primary beat followed by a softer secondary beat. This can be achieved by adding two waves with different amplitudes and timings.
Example: Heartbeat simulation. This example uses two SineWave units: one for the primary
beat one for the secondary beat. The bpm()
function sets the frequency of the waves in beats
per minute.
#include <Plaquette.h>
SineWave primary; // Main heartbeat wave
SineWave secondary; // Secondary beat
AnalogOut led(9); // LED for visualizing the heartbeat
void begin() {
primary.bpm(80); // Set primary beat to 80 beats per minute
secondary.bpm(2*primary.bpm()); // Set secondary beat to twice primary BPM
secondary.amplitude(0.8); // Secondary beat is less strong
}
void step() {
float heartBeat = (primary + secondary) / 2; // Combine and normalize waves
led.put(heartBeat); // Drive LED with combined signal
println(heartBeat); // Stream the combined wave for visualization
}
In this simulation, the primary
sine wave provides the dominant rhythm, while the secondary
sine wave introduces a softer, complementary pulse. The resulting waveform mimics the double-thump
pattern of a human heartbeat.
Try experimenting with different wave types, amplitudes, and frequencies to see how the combined waveform changes. Try adding a third wave, making sure you divide the result by 3 intead of 2. Wave addition opens up endless possibilities for creating expressive and engaging outputs.
Modulation
Modulation involves using one oscillator to influence the properties of another, creating rich and dynamic effects. For example, a slower wave (also called a Low-Frequency Oscillator (LFO)) can modulate the frequency, phase, period, amplitude, or width of a faster wave.
Example: Modulate the frequency of a sine wave with a triangle wave:
#include <Plaquette.h>
TriangleWave modulator(10.0); // LFO (10 seconds period)
SineWave sine; // Main wave
AnalogOut led(9); // LED output
void begin() {}
void step() {
sine.frequency(modulator.mapTo(1.0, 10.0)); // Modulate frequency between 1 and 10 Hz
sine >> led; // Drive LED with modulated sine wave
println(sine); // Stream the modulated wave
}
Adding Noise with randomFloat()
While oscillators are incredibly useful for generating regular and predictable waveforms, there are
times when you may want to introduce randomness to add a sense of natural variation or lifelike behavior.
Plaquette provides the randomFloat()
function, which is a powerful tool for generating random values.
Warning
Avoid using Arduino’s random() function as it returns integer numbers instead of floating-point numbers.
The randomFloat()
function can be used in several ways:
randomFloat()
generates a random float between 0.0 and 1.0.randomFloat(max)
generates a random float between 0.0 andmax
.randomFloat(min, max)
generates a random float betweenmin
andmax
.
These random values can be used to add noise directly to a signal.
Example: Add noise to a sine wave.
#include <Plaquette.h>
SineWave wave(1.0); // Base waveform
AnalogOut led(9); // LED output
void begin() {}
void step() {
float noise = randomFloat(-0.1, 0.1); // Generate noise value in [-0.1, 0.1]
float noisyWave = wave + noise; // Compute sine value + noise
noisyWave >> led; // Drive LED with noisy sine wave
println(noisyWave); // Stream the noisy sine wave
}
These random values can also be used to modify properties such as amplitude, frequency, width, or phase.
Example: Update the wave’s period according to a random walk. The potentiometer controls the amount of noise.
#include <Plaquette.h>
AnalogIn pot(A0); // Potentiometer input
SineWave wave(1.0); // Wave with initial period of 1 second
AnalogOut led(9); // LED output
void begin() {}
void step() {
float noise = randomFloat(-pot, pot); // Generate noise according to potentiometer value
wave.period( wave.period() + noise ); // Add noise to period
wave >> led; // Drive LED with noisy sine wave
println(wave); // Stream the sine wave
}
Example: Introduce randomness to the frequency of a triangle wave. Frequency updated on each push of the button.
#include <Plaquette.h>
DigitalIn button(2, INTERNAL_PULLUP); // Button input
TriangleWave wave; // Wave with default properties
AnalogOut led(9); // LED output
void begin() {
button.debounce(); // Debounce button
wave.frequency(5.0); // Start at 5 Hz
}
void step() {
if (button.rose()) {
wave.frequency(randomFloat(4.0, 6.0)); // Random frequency between 4 and 6 Hz
}
println(wave); // Stream the wave for visualization
}
Randomness can also be combined with modulation to create highly dynamic and expressive behaviors. Experiment with adding random noise to various properties and observe the effects using the Serial Plotter. Try to simulate a natural phenomena like a flickering flame or a lightning bolt.
Timing Functions
Oscillators offer various timing functions to control their behavior:
start(): Starts/restarts the oscillator.
stop(): Stops it and resets it.
pause(): Pauses the wave at its current point.
resume(): Resumes from the paused point.
togglePause(): Toggles between paused and running states.
isRunning(): Returns whether the oscillator is active.
setTime(): Sets the current phase of the oscillator based on absolute time (in seconds).
Example: Use the button to start and stop the wave:
#include <Plaquette.h>
DigitalIn button(2, INTERNAL_PULLUP); // Button input
SineWave sine; // Wave with default properties
AnalogOut led(9); // LED output
void begin() {
sine.frequency(2.0); // Initialize frequency to 2 Hz
}
void step() {
if (button.rose()) {
sine.togglePause(); // Pause or resume the wave
}
sine >> led; // Drive LED with sine wave
println(sine); // Stream the wave for visualization
}
Phase Shifting with shiftBy()
The shiftBy()
function allows you to offset the phase of an oscillator relative to its current
position and returns the value of the dephased wave. This is useful for creating complex, synchronized
patterns.
Example: Shift the phase of a sine wave:
#include <Plaquette.h>
SineWave wave(5.0); // Sine wave with 5 seconds period
void begin() {}
void step() {
// Print shifted values separated by white spaces.
print(wave); print(" "); // 0% shift
print(wave.shiftBy(0.25)); print(" "); // 25% shift
print(wave.shiftBy(0.5)); print(" "); // 50% shift
println(wave.shiftBy(0.75)); // 75% shift
}
Conclusion
Oscillators are powerful tools for creating dynamic, expressive systems. By combining their waveforms, timing functions, and phase-shifting capabilities, you can achieve intricate and synchronized behaviors. Modulation and randomness add another layer of complexity, enabling you to create engaging and responsive media systems. Explore these features in Plaquette and see how waves can bring your projects to life.