Abstract
Tones of a specified frequency can easily be produced on nearly all mcus (microcontrollers) using pwm (pulse-width modulation). The output is a square wave. This post explores the use of sampling to produce frequency waves. Although this is more processor-intensive method, it does allow for greater control of the output wave. This post introduces the concept of “boolean-modulation”, where the on-off states of an input square-wave signal is processed by boolean logic. It demonstrates how to produce an effect that is similar to a binaural beat. Additional harmonics are introduced into the output, giving them a “funkier” effect.
Algorithm for producing sampled square waves
Suppose the following:
- fs is the desired sampling frequency of the wave. fs = 44100 (Hz) is a typical sample rate for many audio files
- fw_t is the frequency of the square wave to output. fw_t = 440 (Hz) is the frequency of the note “A”. More specifically, it is usually referred to “A4”.
The following algorithm, written in D, determines whether the output should be 0 or 1:
import std.math;
import std.stdio;
const float fw = 440; // frequency of wave
const float fs = 44100; // sample frequency
const int nsamples = 60 * cast(int)fs; // reserve space for 60 secs of tone
void main()
{
float[] samples = new float[nsamples]; // the output
float dt = 1 / fs; // time increment is inverse of sampling freq
float t = 0; // time
const float fw_t = 1.0/fw; // period of output wave
for(int i=0; i< nsamples; i++) {
t = fmod(t, fw_t); // limit time to one wave period
samples[i] = 0;
if(t>=0.5*fw_t) samples[i] = 1.0; // in second-half of wave
t += dt; // increment time
}
... any extra processing, e.g. writing to file
}
It is an algorithm that can be translated easily to a microcontroller with a timer. The output is a 50% duty cycle. It can be changed easily.
Boolean modulation
Binaural beats are generated when two sine waves of similar, but non-identical, sine waves are added together. This method is not especially suited to microcontrollers because the output is of variable amplitude rather than a simple on/off of a GPIO pin.
However, all is not lost. The inspiration behind binaural beats can be modified to use simple boolean logic. The strategy is to generate two square waves of slightly different frequency. Their on/off states are OR’d together to produce the output state.
Here is example code:
import waved;
import std.math;
import std.stdio;
const float fw = 500; // frequency of wave
const float fs = 44100; // sample frequence
const int nsamples = 60 * cast(int)fs; // 60s
void main()
{
float[] samples = new float[nsamples];
float dt = 1 / fs;
float t0 = 0, t1 = 0; // time of each of the two waves
const float fw_t0 = 1.0/fw;
const float fw_t1 = 1.0/(fw + 1.0); // add a little binaural
for(int i=0; i< nsamples; i++) {
samples[i] = 0;
bool hi0= false;
t0 = fmod(t0, fw_t0);
if(t0>=0.5*fw_t0) hi0 = true;
t0 += dt;
bool hi1 = false;
t1 = fmod(t1, fw_t1);
if(t1>=0.5*fw_t1) hi1 = true;
t1 += dt;
if(hi0 || hi1) samples[i] = 0.1;
}
writeln("sample array filled");
Sound snd= Sound(cast(int)fs, 1, samples);
encodeWAV(snd, "out.wav");
writeln("Done");
}
The frequency in this case is 500Hz. There is a small offset frequency – 1 Hz works well – that modifies the on/off state.
A 60 sec sample output is available here (470k size).
The source code is available here.
Parting remarks
This is an efficient way of generating waves with interesting tonal qualities for use in note production.
Ref: db7.102