The sound of Erlang: How to use Erlang as an instrument
- Erlang Solutions Team
- 8th Sep 2020
- 28 min of reading time
When most people think of Erlang, they think of enormous business platforms handling high volumes of concurrent users. That’s often accurate, but we wanted to show off a less conventional but fun project that you can do in Erlang. Sam Aaron’s Sonic Pi is a wonderful code-based music creation and performance tool. It’s a brilliant innovation that makes use of Erlang, and it could be a big part of the future of music. You can see an example of what Sonic Pi is capable of here. Also, you can join us at Code Mesh V conferecne on 5-6 November for Sam’s tutorial – Introduction to Live Coding Music With Sonic Pi.
We were inspired by this and decided to go one step further and show off how you can code music in Erlang directly.
This is a table of all the software used with their versions.
Before we make music with Erlang, it’s worth explaining some of the underlying theory behind what sound actually is; this will come in handy later.
Sound is a vibration that propagates as an acoustic wave.
Frequency is the number of occurrences of a repeating event per unit of time. Its basic unit is Hz which is the number of occurrences per second.
f = 1 / T
The period is the duration of time of one cycle in a repeating event. The period is the reciprocal of the frequency.
where:
The simplest way of generating a wave is by providing a sine wave with the given frequency.
Computers work in a discrete domain while sine waves work in a continuous domain; therefore, sampling is used to convert material from a continuous domain to a discrete one.
Sampling sound is the process of converting a sound wave into a signal.
The more samples we approximate, the better our sound quality will be, but this will also make the file in which we store the approximations larger.
If you want to learn more about sampling I recommend watching this video.
Start by creating a new project.
Since the script is small, I will use the escript
template:
rebar3 new escript the_sound_of_erlang
Let’s check if the project worked correctly:
cd the_sound_of_erlang
rebar3 escriptize
./_build/default/bin/the_sound_of_erlang
mkdir out
If everything has gone to plan, then you should see the following output:
$ rebar3 escriptize
===> Verifying dependencies...
===> Compiling the_sound_of_erlang
===> Building escript...
$ ./_build/default/bin/the_sound_of_erlang
Args: []
Well done!
Lets now generate an example wave. To do this we can just:
wave() ->
[math:sin(X) || X <- lists:seq(1, 48000)].
To save a generated wave, we need to transform a list of floats to binary representation and write this binary to a file.
save(Filename, Wave) ->
Content = lists:foldl(
fun(Elem, Acc) ->
<<Acc/binary, Elem/float>> end,
<<"">>, Wave),
ok = file:write_file(Filename, Content).
Call the above 2 functions in the main/1
function as follows:
main(_) ->
Wave = wave(),
save("out/first_wave.raw", Wave),
erlang:halt(0).
Build the script binary with:
rebar3 escriptize
And run it with:
./_build/default/bin/the_sound_of_erlang
A new file called out/first_wave.raw should show up in a repository. Then use ffplay
to listen to the result:
ffplay -f f64be -ar 48000 out/first_wave.raw
The options given are:
-f f64be
means that input format is 64-bit big-endian float-ar 48000
means that the input audio sampling rate is 48000 per secondYou can listen to the result in the video below.
It’s not a pleasant sound (yet), but it’s a start.
For the sake of convenience, let’s try to play the resulting sound from the script:
play(Filename) ->
Cmd = "ffplay -f f64be -ar 48000 " ++ Filename,
os:cmd(Cmd).
and add it to the end of main/1
:
main(_) ->
Wave = wave(),
Filename = "out/first_wave.raw",
save(Filename, Wave),
play(Filename),
erlang:halt(0).
Now we can recompile the script and run it with:
rebar3 escriptize && ./_build/default/bin/the_sound_of_erlang
We can also convert a raw file to a .mp3 format with:
ffmpeg -f f64be -ar 48000 -i out/first_wave.raw out/first_wave.mp3
That will enable it to be played with any music player.
We have managed to generate a wave, save it to a file and play it.
Now that we can make sound let’s improve our wave, so it’s not just any random sound, but a fixed frequency for a given amount of time.
To have several samples played in a given amount of time, we need to multiply the sample rate times and sound duration, since the number of samples is an integer we should round the multiplication result.
NumberOfSamples = round(SampleRate * Duration)
The sinus period is 2 * PI, because we know that and the sample rate, we can calculate how long each signal step will last for a given frequency Hz.
Step = Hz * 2 * math:pi() / SampleRate
Knowing the number of samples and the Step, we can map the time domain to a signal as follows:
frequency(Hz, Duration, SampleRate) ->
Signals = lists:seq(1, round(SampleRate * Duration)),
Step = Hz * 2 * math:pi() / SampleRate,
[ math:sin(Step * Signal) || Signal <- Signals ].
Let’s now modify a wave/0
function to get a sound of 440 Hz played for 2 seconds with a sampling rate of 48000 samples per second:
wave() ->
frequency(440, 2, 48000).
I will change the Filename
in the main/1
function to
Filename = "out/2Sec440Hz.raw",
Just add it to a repository.
You can listen to the result below.
Let’s play the new sound and compare it with the same frequency of sound from YouTube.
To me, they sound identical.
rebar3 escriptize && ./_build/default/bin/the_sound_of_erlang
We can now try playing two sounds with different frequencies and lengths of times, but we need to flatten the list of signals to make a list of signals from a list of lists of signals.
Filename = "out/2Sec440HzAnd1Sec500Hz.raw",
...
wave() ->
lists:flatten([
frequency(440, 2, 48000)
, frequency(500, 1, 48000)
]).
You can listen to the result below.
We can play a given frequency for a given amount of time, but how do we make music out of that?
From here we can see that the frequency of an A4 note is 440 Hz
which is also known as pitch standard.
We can also lookup all other needed notes in the same way, but there is an alternative called the frequency ratio of a semitone, and it is equal to the twelfth root of two 2 ** (1 / 12)
.
To calculate A#4 which is 1 semitone higher than A4 we just multiply 440 * (2 ** (1/12)) = 466.16
and, after comparing it to the table the value is A#4 the correct corresponding frequency.
Let’s try translating the maths into code:
At the top of the file just below the module directive, we can add a macro for pitch standard and extract the sampling rate.
-module(the_sound_of_erlang).
-define(PITCH_STANDARD, 440.0).
-define(SAMPLE_RATE, 48000).
-export([main/1]).
Now we need to use the macro for the sampling rate in the code.
Let’s modify frequency/3
to frequency/2
:
frequency(Hz, Duration) ->
Signals = lists:seq(1, round(?SAMPLE_RATE * Duration)),
Step = Hz * 2 * math:pi() / ?SAMPLE_RATE,
[ math:sin(Step * Signal) || Signal <- Signals ].
Do not forget to change the calls of the frequency function by removing the last argument:
wave() ->
lists:flatten([
frequency(440, 2)
, frequency(500, 1)
]).
and play/1
as follows:
play(Filename) ->
StrRate = integer_to_list(?SAMPLE_RATE),
Cmd = "ffplay -f f64be -ar " ++ StrRate ++ " " ++ Filename,
os:cmd(Cmd).
The following function takes the number of semitones to be shifted and returns a frequency of a shifted sound:
get_tone(Semitones) ->
TwelfthRootOfTwo = math:pow(2, 1.0 / 12.0),
?PITCH_STANDARD * math:pow(TwelfthRootOfTwo, Semitones).
We need to introduce one more concept, beats per minute, which is the base time unit for a note to be played.
Each note is played in a given number of beats and the number of beats per minute is fixed, so we can calculate how long each beat lasts (in seconds) by dividing 60 by beats per minute.
Let’s introduce a new function for that:
beats_per_minute() ->
120.
beat_duration() ->
60 / beats_per_minute().
We can generate notes for a given amount of time with the following function:
sound(SemitonesShift, Beats) ->
frequency(get_tone(SemitonesShift), Beats * beat_duration()).
Let’s try it out by providing the following wave:
wave() ->
lists:flatten([
sound(SemiTone, 1) || SemiTone <- lists:seq(0, 11)
]).
And play it by recompiling and running the script.
I saved my output as "out/increasingSemitones.raw"
.
You can listen to the below.
I only need some of these notes to play my songs, but you might need more, so I provided them in the semitones_shift/1
function. Let’s provide a helper function for easier sound notation:
note(Note) ->
SemitonesShift = semitones_shift(Note),
get_tone(SemitonesShift).
semitones_shift(c4) -> -9;
semitones_shift(c4sharp) -> -8;
semitones_shift(d4flat) -> -8;
semitones_shift(d4) -> -7;
semitones_shift(d4sharp) -> -6;
semitones_shift(e4flat) -> -6;
semitones_shift(e4) -> -5;
semitones_shift(f4) -> -4;
semitones_shift(f4sharp) -> -3;
semitones_shift(g4flat) -> -3;
semitones_shift(g4) -> -2;
semitones_shift(g4sharp) -> -1;
semitones_shift(a4flat) -> -1;
semitones_shift(a4) -> 0;
semitones_shift(a4sharp) -> 1;
semitones_shift(b4flat) -> 1;
semitones_shift(b4) -> 2;
semitones_shift(c5) -> 3;
semitones_shift(c5sharp) -> 4;
semitones_shift(d5flat) -> 4;
semitones_shift(d5) -> 5;
semitones_shift(d5sharp) -> 6;
semitones_shift(e5flat) -> 6;
semitones_shift(e5) -> 7;
semitones_shift(f5) -> 8;
semitones_shift(f5sharp) -> 9;
semitones_shift(g5flat) -> 9;
semitones_shift(g5) -> 10;
semitones_shift(g5sharp) -> 11;
semitones_shift(a5flat) -> 11;
semitones_shift(a5) -> 12.
and modify slightly the sound/2
function as follows:
sound(Note, Beats) ->
frequency(note(Note), Beats * beat_duration()).
To use the more convenient, newly created note/1
function instead of get_tone/1
.
Now we can try out the sounds played by modifying the wave/0
function as follow:
wave() ->
lists:flatten([
sound(Note, 1) || Note <- [
c4, c4sharp, d4flat, d4, d4sharp, e4flat,
e4, f4, f4sharp,g4flat, g4, g4sharp,
a4flat, a4, a4sharp, b4flat, b4
]
]).
I will save the result in "out/increasingNotes.raw"
file.
You can listen to the result below.
When you listen to the increasing notes, you will notice that there is a very strange tick or thudding sound as the note changes.
This is because the sound increases and decreases too rapidly.
To resolve this issue, we can implement ADSR which stands for *A*ttack *D*ecay *S*ustain *R*elease and works by modifying the sound amplitude (volume) according to the following chart:
For the sake of simplicity, it is enough to only implement the Attack and Release components because we already have the Sustain part.
To implement the Attack, we will consider a sequence of numbers smaller or equal to 1 that will be generated in the following way:
attack(Len) ->
[min(Multi / 1000, 1) || Multi <- lists:seq(1, Len)].
An example of this kind of list may look like this:
[0.001, 0.002, ... 0.999, 1, 1, 1, ..., 1]
We can also generate the Release a lazy way:
release(Len) ->
lists:reverse(attack(Len)).
The release function generates the following list:
[1, 1, 1, ..., 1, 0.999, ..., 0.002, 0.001]
Now we need to slightly modify the frequency/2
to adjust the sound volume:
frequency(Hz, Duration) ->
Signals = lists:seq(1, round(?SAMPLE_RATE * Duration)),
Step = Hz * 2 * math:pi() / ?SAMPLE_RATE,
RawOutput = [ math:sin(Step * Signal) || Signal <- Signals ],
OutputLen = length(RawOutput),
lists:zipwith3(
fun(Attack, Release, Out) -> Attack * Release * Out end,
attack(OutputLen), release(OutputLen), RawOutput).
I saved the result as out/increasingNotesASR.raw
. Now when you rebuild and run the script, you will hear a smooth transition as we pass between the notes.
Now we will try to play the actual song.
Let’s modify the wave/0
function as follows:
wave() ->
lists:flatten([
sound(f4, 0.5)
, sound(d4, 0.5)
, sound(d4, 0.5)
, sound(d4, 0.5)
, sound(g4, 2)
, sound(d5, 2)
, sound(c4, 0.5)
, sound(b4, 0.5)
, sound(a4, 0.5)
, sound(g5, 2)
, sound(d5, 1)
, sound(c4, 0.5)
, sound(b4, 0.5)
, sound(a4, 0.5)
, sound(g5, 2)
, sound(d5, 1)
, sound(c4, 0.5)
, sound(b4, 0.5)
, sound(c4, 0.5)
, sound(a4, 2)
, sound(d4, 1)
, sound(d4, 0.5)
]).
Also, change the beat per minute to 120. The reasoning behind setting given beats per second can be found here but it is out of the scope of this article so I will not go into further details.
beats_per_minute() -> 120.
You can recompile and run the script or just listen to the result below.
The result can be saved to out/StarErlang.raw. I hope you recognize the melody I picked, it is the Star Wars Theme.
Last but not least, let’s introduce an Erlang behaviour for a melody.
Create a new file src/melody.erl
and define a melody behaviour.
-module(melody).
-type note() :: c4 | c4sharp | d4flat | d4 | d4sharp | e4flat | e4 |
f4 | f4sharp | g4flat | g4 | g4sharp | a4flat | a4 |
a4sharp | b4flat | b4 | c5 | c5sharp | d5flat | d5 |
d5sharp | e5flat | e5 | f5 | f5sharp | g5flat | g5 |
g5sharp | a5flat | a5.
-type duration() :: float().
-callback beats_per_minute() -> non_neg_integer().
-callback sounds() -> {note(), duration()}.
Each melody may have different beats per minute, so this is the first function is needed to describe a song and the second function is the notes the song consists of.
There are two types to be introduced: note()
which is one of the possible notes (aka sound frequencies) and the duration()
which is a float saying how many beats the sound of a given frequency will last.
To use a song defined in a different module with a slightly simplified notation, let’s add a new macro which will store the module name in which the song is defined:
-define(SONG, star_wars_main_theme).
and modify wave/0
and beats_per_minute/0
functions to use it:
beats_per_minute() ->
?SONG:beats_per_minute().
wave() ->
RawSounds = ?SONG:sounds(),
Sounds = lists:map(
fun({Note, Duration}) ->
sound(Note, Duration)
end, RawSounds),
lists:flatten(Sounds).
This will not work yet as there is no star_wars_main_theme module defined, so
create a file src/songs/star_wars_main_theme.erl
and implement the melody
behavior:
-module(star_wars_main_theme).
-behaviour(melody).
-export([sounds/0, beats_per_minute/0]).
beats_per_minute() ->
120.
sounds() ->
[
, {d4, 0.5}
, {d4, 0.5}
, {d4, 0.5}
, {g4, 2}
, {d5, 2}
, {c4, 0.5}
, {b4, 0.5}
, {a4, 0.5}
, {g5, 2}
, {d5, 1}
, {c4, 0.5}
, {b4, 0.5}
, {a4, 0.5}
, {g5, 2}
, {d5, 1}
, {c4, 0.5}
, {b4, 0.5}
, {c4, 0.5}
, {a4, 2}
, {d4, 1}
, {d4, 0.5}
, {g4, 2}
, {d5, 2}
, {c4, 0.5}
, {b4, 0.5}
, {a4, 0.5}
, {g5, 2}
, {d5, 1}
, {c4, 0.5}
, {b4, 0.5}
, {a4, 0.5}
, {g5, 2}
, {d5, 1}
, {c4, 0.5}
, {b4, 0.5}
, {c4, 0.5}
, {a4, 2}
, {d4, 1}
, {d4, 0.5}
, {e4, 1.5}
, {e4, 0.5}
, {c4, 0.5}
, {b4, 0.5}
, {a4, 0.5}
, {g4, 0.5}
, {g4, 0.5}
, {a4, 0.5}
, {b4, 0.5}
, {a4, 1}
, {e4, 0.5}
, {f4sharp, 1}
, {d4, 1}
, {d4, 0.5}
].
You can listen to the result below.
We have also provided a few more examples.
We hope you enjoyed this tutorial, If you want to see the full code go to https://github.com/aleklisi/The-Sound-Of-Erlang we’re looking forward to hearing your examples of your favourite song, played in Erlang. You can share them with us on Twitter.
Erlang is a programming language designed to offer concurrency and fault-tolerance, making it perfect for the needs of modern computing. Talk to us about how you can handle more users, safer, faster and with less physical infrastructure demands. Find out how our experts can help you.
We pride ourselves on sharing our knowledge to help people make the most of BEAM technologies. Our team of trainers includes some of the most...
Discover the big brands reaping significant benefits by using Erlang in production.