Make with Ada : From bits to music
by Raphaël Amiard –
I started out as an electronic musician, so one of my original motivations when I learnt programming was so that I could eventually *program* the sounds I wanted rather than just use already existing software to do it.
If you know sound synthesis a bit, you know that it is an incredibly deep field. Producing a simple square wave is a very simple task, but doing so in a musical way is actually much more complex. I approached this as a total math and DSP newbie. My goal was not to push the boundaries of digital sound synthesis, but rather to make simple stuff, that would help me understand the underlying algorithms, and produce sound that could be labelled as musical.
Also, even if I'm bad at math, I picture myself as reasonably good at software architecture (don't we all!), and I figured that producing a simple sound synthesis library, that would be easy to use and to understand, and actually small enough not to be intimidating, would be a reasonable milestone for me.
One of the other objectives was to make something that you could run on a small bareboard computer such as the stm32 or the raspberry pi, so it needs to be efficient, both in terms of memory and CPU consumption. Being able to run without an operating system would be a nice plus too!
And this is how ada-synth-lib was born, for lack of a better name. The aim of the library is to present you with a toolkit that will allow you to produce sound waves, massage them into something more complex via effects and envelopes, regroup them as instruments, but also sequence them to produce a real musical sequence.
But let's walk through the basics! We'll see how to build such a sequence with ada-synth-lib, from a very simple sine generator, to a full musical sequence.
Preamble: How to compile and run the library
As its name indicates, ada-synth-lib is developped in Ada, using the AdaCore Libre suite of tools. To build and run the examples, you'll need the GPL 2015 edition of the AdaCore libre release, that you can get from here: AdaCore libre site
Starting simple: The sine generator
Starting simple, we're gonna just generate a sound wave, and to make the sound not too aggressive to your ears, we'll use a sine wave, that has a smooth and soothing profile.
If you know something about sound theory, you may know that you can recreate any (periodical) sound from a carefully arranged superposition of sine waves, so the choice of the sine wave is also a way of paying respect to the theory of sound generation in general, and to fourier in particular.
Here is the code to generate a simple sine wave with ada-synth-lib. We just have a very simple sine generator, and we use the `Write_To_Stdout` helper to write the sound stream on the standard output.
with Waves; use Waves;
with Write_To_Stdout;
procedure Simple_Sine is
-- Create a simple sine wave Generator.
Sine_Gen : constant access Sine_Generator := Create_Sine (Fixed (300.0));
begin
Write_To_Stdout (Sine_Gen);
end Simple_Sine;
Compiling this example and running it on the command line is simple, but we are just creating a sound stream and printing it directly to stdout! To hear it on our speakers, we need to give it to a program that will forward it to your audio hardware. There are several options to do that, the most known being the old /dev/dsp file on Unix like systems, but you have great cross platform tools such as sox that you can use for such a purpose.
# you should hear a sine !
$ obj/simple_sine | play -t s16 -r 44100 -
The interesting thing is that the input to `Create_Sine` is another generator. Here we use a fixed value generator, that will provide the value for the frequency, but we could use a more complex generator, which would modulate the input frequency!
with Waves; use Waves;
with Write_To_Stdout;
procedure Simple_Sine is
Sine_Gen : constant access Sine_Generator :=
Create_Sine (
Fixed
(1000.0,
-- The second parameter to the Fixed constructor is a generator
-- that will be added to the fixed frequency generated.
-- LFO is also a sine oscillator underneath, but you can set it to
-- have amplitudes much larger than +- 1.0
LFO (6.0, 200.0)));
begin
Write_To_Stdout (Sine_Gen);
end Simple_Sine;
Going deeper
This is just the beginning of what you can do. ada-synth-lib is just a lego toolkit that you can assemble to generate the sequences you want to generate.
With only slightly more complex sequences, you can get into real musical sequences, such as the one you can hear below:
The sequencing part is done via the simple sequencer data type which you can use to create looping note sequences. Here is how it is done for the snare instrument:
o : constant Sequencer_Note := No_Seq_Note;
K : constant Sequencer_Note := (Note => (G, 3), Duration => 3000);
Z : constant Sequencer_Note := (Note => (G, 3), Duration => 5000);
B : constant Sequencer_Note := (Note => (G, 3), Duration => 8000);
Snare_Seq : constant access Simple_Sequencer :=
Create_Sequencer
(Nb_Steps => 16, BPM => BPM, Measures => 4,
Notes =>
(o, o, o, o, Z, o, o, o, o, o, o, o, K, o, o, o,
o, o, o, o, K, o, o, o, o, o, o, o, B, o, K, K,
o, o, o, o, Z, o, o, o, o, o, o, o, K, o, o, o,
o, o, o, o, K, o, o, K, o, o, Z, o, B, o, Z, o));
You can also see how we used Ada's named aggregates to make the code more readable and self documenting. Also interesting is how we can create complex synth sounds from basic bricks, as in the below example. The bricks are very simple to understand individually, but the result is a full substractive synthetizer that can be programmed to make music!
Synth : constant access Disto :=
-- We distort the output signal of the synthetizer with a soft clipper
Create_Dist
(Clip_Level => 1.00001,
Coeff => 1.5,
-- The oscillators of the synth are fed to an LP filter
Source => Create_LP
(
-- We use an ADSR enveloppe to modulate the Cut frequency of the
-- filter. Using it as the modulator of a Fixed generator allows us
-- to have a cut frequency that varies between 1700 hz and 200 hz.
Cut_Freq_Provider =>
Fixed
(Freq => 200.0,
Modulator => new Attenuator'
(Level => 1500.0,
Source => Create_ADSR (10, 150, 200, 0.005, Synth_Source),
others => <>)),
-- Q is the resonance of the filter, very high values will give a
-- resonant sound.
Q => 0.2,
-- This is the mixer, receiving the sound of 4 differently tuned
-- oscillators, 1 sine and 3 saws
Source =>
Create_Mixer
(Sources =>
(4 => (Create_Sine
(Create_Pitch_Gen
(Rel_Pitch => -30, Source => Synth_Source)),
Level => 0.6),
3 => (BLIT.Create_Saw
(Create_Pitch_Gen
(Rel_Pitch => -24, Source => Synth_Source)),
Level => 0.3),
2 => (BLIT.Create_Saw
(Create_Pitch_Gen
(Rel_Pitch => -12, Source => Synth_Source)),
Level => 0.3),
1 => (BLIT.Create_Saw
(Create_Pitch_Gen
(Rel_Pitch => -17, Source => Synth_Source)),
Level => 0.5)))));
The ADSR envelope is what gives the sound a dynamic nature, shaping it in the time domain. The Low Pass filter shapes the sound by removing some high frequency components from it.
That's it for today! In the next instalment of this series we'll see how to compile and run the code on a bare board system using the STM32F4 board and AdaCore GPL tools.
Links & Credits
- You can find the ada-synth-lib library on github here.
- The needed toolchain to play with it is on the libre site.
- A good, high-level guide to music synthesis here.
- A lot of the algorithms in ada-synth-lib were inspired by stuff I found on http://musicdsp.org/, so big thanks to every people putting algorithms in there.
- The alias free oscillators in the BLIT module are done using the Bandlimited Impulse Train method, for which the seminal paper is here.
- Thanks and credits to Mikael Altermark for the beautiful sound waves pictures, and to Bisqwit for the introduction video!