#rakulang Puttering about with digital signals

I’m in the process of relearning about DSP (Digital Signal Processing). alas, I’ve forgotten nearly everything that I learned initially at university. That was 30 years ago, mind.

One aspect we might like to consider is how to produce tones using MCUs. The easiest to produce is a square wave. I’m going to consider using actual MCUs at some future point, but I think it’s a little easier to explore things first using software.

Let’s use Raku to produce a simple square wave. Here’s a suitable function:

sub sq-wave($samf, $dur, $freq) {
         my $n = $samf/(2*$freq);
         my $single-wave = flat -1.0 xx $n, 1.0 xx $n;
         my $reps = $freq * $dur;
         return ($single-wave xx $reps).flat;

The function takes a sampling frequency (samf), a duration of the note (dur), and the frequnecy of the note (freq). The sampling frequency represents how often we sample the input wave. I typically choose a frequency of 8KHz, because it is the default rate expected by the Linux utility aplay and is low enough that we stand a fighting chance of doing some processing on an MCU.

People, mostly confined to the very young, are potentially capable of detecting notes up to 22KHz, so there will be some notes that we cannot produce that people might be able to hear.

The function above produces amplitudes that swing backwards and forwards between -1 and +1. Suppose we want to produce a note with a frequency of 440Hz. Then we could run these commands:

 my $sample-freq = 8000;
 my $dt = 1/$sample-freq;
 my @wav1 = sq-wave($sample-freq, 10, 440);

We can save this wave using a function like:

sub save(@wav, $name, $dt) {
        my $name1 = "/tmp/$name";

        # save raw audio for playing
        my @raw = scale-array @wav, 0 , 255;
        @raw = @raw.map({$_.Int});
        my $blob = blob8.new(@raw); 
        spurt ($name1 ~ ".raw"), $blob;

        # save the first 100 samples for plotting
        my $fout = open ($name1 ~ ".dat"), :w;  
        for (0..100) -> $i {
                my $t = $i * $dt;
                $fout.print("$t @wav[$i]\n");

Calling it is straight-forward:

save @wav1, "sqr", $dt;

This will create two files. Firstly, it will create a file called /tmp/sqr.raw . This file is suitable for playing directly by “aplay”. Just type:

aplay /tmp/sqr.dat

“save” takes the wave data, and rescales them to unsigned bytes.

The sub creates another file with an ascii format that we can pass to “gnuplot”. Only the first 101 samples are saved so as to make the plot legible. You can see a plot of the square wave in the first plot below:

“scale-array” is implemented as follows:

sub scale($x, $x-lo, $x-hi, $lo, $hi)
        return $lo + ($hi - $lo) * ($x - $x-lo) / ($x-hi - $x-lo);

sub scale-array(@arr, $lo, $hi)
        my $min = min @arr;
        my $max = max @arr;
        return @arr.map({scale $_, $min, $max, $lo, $hi});

The sound is perfectly acceptable, but we might want to go further. The discontinuities – the jumps – create high-frequencies that can sound harsh to the ears. I would like to investigate this in a further post when I hope to delve more into Fourier Analysis.

Can we smooth out this wave, bet rid of those high frequencies? The answer is “yes”. We need to apply a “low pass filter”, which is to say, a “filter” that lets the low frequencies through, whilst eliminating high frequencies.

Electronically, a low pass filter can be created using an “RC circuit”, so-called because it consists of a resistor (“R”) and a capacitor (“C”). In fact, that was exactly the solution I adopted in a previous post, where I showed how to construct a brown noise generator from white noise created by a humble ATTiny85 MCU. I still have that device. Indeed, I use it nightly, and I am satisfied with its performance. I think my design might be a little faulty, though, and could benefit from an OPAMP. That’s a discussion for another time, though.

Let’s take another tack in this post. Instead of filtering the signal electrically, let’s perform the filtering using a compute. How do we do this? Well, the simple expedient of performing numerical integration does the trick:

sub integrate(@wav, $dt) {
        my @res;
        my $y = 0;
        my $t = 0;

        for @wav { $t += $dt; $y += $dt* $_ ; @res.append( $y); }
        return @res;

We’ll want to rescale our results, too, so that the results are in the range [-1, 1]:

sub scale-integrate(@arr, $dt)
        return scale-array (integrate @arr, $dt), -1.0, 1.0;

Therefore, to integrate and save our square wave:

my @wav2 = scale-integrate @wav1, $dt;
save @wav2, "tri", $dt;

This results in a triangular wave, shown as the second plot in the diagram above.

Remember to play the wave and take note of the differences:

aplay /tmp/tri.raw

If we do a Fourier Analysis of the output, we should find that the higher frequencies are much damped-down compared to the original output. Again, that analysis will have to wait until a future occasion (not least because I need to brush up on the topic).

There’s a certain amount of “cheating” going on. Notice that the processed wave was “renormalised” to give values between -1 and -1. For now, we have the luxury of examining the whole wave and don’t have a requirement to process the signal real-time.

Doing this on a microcontroller would present a number of challenges to overcome: it must be done in real-time, the processor we choose might only have a clock frequency of 8MHz or even just 1MHz, very little memory will be available, and is highly unlikely to have an FPU (Floating Point Unit). So we would need to do our rescaling having first selected suitable range lows, highs and scaling factors.

So, we have our triangle wave. What if we wanted a sine wave? Well, that’s easy. we perform the integration once again:

my @wav3 = scale-integrate @wav2, $dt;
save @wav3, "sin", $dt;

This produces the last plot in the list, which is hard to distinguish against a true sine wave. Don’t forget to play the wave, which is all part of the fun of tinkering with this stuff:

aplay /tmp/sin.raw

So, cool, we started out with a square wave, produced a triangular wave out of it, and finally a sine (approximate) wave, no trigonometry involved. Although the ultimate outcome was to produce a sine wave, square and triangular audio waves can still be interesting in their own right.

The complete Raku code is available here. The gnuplot file that produces the plots above is available here. It is a simple file:

set multiplot layout 3,1
plot '/tmp/sqr.dat' using 1:2 pt 6
plot '/tmp/tri.dat' using 1:2 with lines
plot '/tmp/sin.dat' using 1:2 with lines
unset multiplot

that you can run by starting gnuplot and typing

load "plot.gnu"

There’s plenty of ground to develop the ideas in here further. The first is a Fourier Analysis of the three types of waves. A second would be an exploration of “noise” and their “colours”. A third would be that of simulating electrical circuits.

Lastly, we might want to investigate the effects of a high-pass filter. This would attenuate the low frequencies, and amplify the higher ones. Although I suggested that higher frequencies are “bad”, people have used them to create resonance effects that are pleasing musically.

I hope to pick up on the topic at some future time. I’d certainly need to do more reading first, though.

Anyway, I hope you guys found it interesting. It seems that the Raku folks out there are a curious lot, which is why I’ve had a bias towards writing posts which feature Raku.

As always, proof-reading has been left as an exercise for the reader.

About mcturra2000

Computer programmer living in Scotland.
This entry was posted in Uncategorized. Bookmark the permalink.

1 Response to #rakulang Puttering about with digital signals

  1. Pingback: 2021.03 Course Topped – Rakudo Weekly News

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s