Audio From Scratch With Go: Frère Jacques

In the last post I ended by saying we would be able to use GoAudio to generate some simple tunes. In this post we will actually put that to the test. Click the video to hear the end result :)

Frère Jacques | Brother John

Brother John / Frère Jacques is a popular nursing rhyme, and also one of the few things I was taught to play on the piano as a kid. As such, I thought it’d be fitting to try to code this song this time. The notes for playing this are taken from true-piano-lessons.com. The code takes the notes starting in the key of C. (Maybe since it’s in Go, I should have started in G. Feel free to change it).

Generating notes

In the previous posts, we actually ignored the problem of turning notes into frequencies. GoAudio works with given frequencies. So we’ll need to first implement a way to turn the notes into frequencies. For this, I came up with a somewhat hackish solution but I didn’t know a better way other than hardcoding it.

As we’re making the music using the equal-tempered scale we can calculate the frequencies for each note given a reference frequency. I’m using middle C as the reference frequency, which can be derived from concert A (A440) in this way: middleC = (concertA * math.Pow(2, 3/12)) / 2.

To follow along with how we derive the frequencies for each note, I suggest taking a look at wikipedia. The basic idea however is that we have 12 semitones in an octave, so for each octave we have to generate the 12 semitones based off a reference frequency. Confusingly however, an octave starts at C and not at A. (it does loop around) so we get: C D E F G A B. Each time we loop around and get back to C, we move up one octave (meaning higher frequency, thus higher pitch).

You can view a table of all frequencies from C0 (C at octave 0) up to B8 (B at octave 8) here.

The code for this is a bit hacky, so if you have a better way please let me know! :-)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var (
	notes = []string{"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}
)

// GenerateNotes creates the <note,frequency> map based on reference frequency FR.
// The reference frequency is set to 440
func generateNotes() map[string]float64 {
	ni := 0
	concertA := 440.
	middleC := (concertA * math.Pow(2, 3./12.)) / 2.
	FR := middleC
	notemap := map[string]float64{}
	for i := 0; i < 24; i++ {
		FK := FR * math.Pow(2, float64(i)/12.)
		note := notes[ni%len(notes)]
		octave := 4 + i/12
		var octs string
		if octave != 0 {
			octs = strconv.Itoa(octave)
		}
		notemap[note+octs] = FK
		ni++
	}
	notemap["WAIT"] = 0.
	return notemap
}

Here I am generating in both the fourth and fifth octave, from C4 -> B5. If we print the map generated by this code we get (abbreviated)

map[A#4:466.1637615180899 
A#5:932.3275230361797 
A4:439.99999999999994 
A5:879.9999999999999
B4:493.8833012561241 
B5:987.7666025122483 
C#4:277.1826309768721 
C#5:554.3652619537442
...
WAIT:0
]

Also note that I have added WAIT with frequency 0 in there. This is a bit hacky (and unnecessary in Go). I’ve added this so we can have some more wait time between playing different sections of the song. As the default-zero value of a float is zero, and because you can access a map safely even when a key is missing, this was not necessary. We could just call the map with the WAIT key even if it wasn’t inserted and our result would still be correct.

1
2
3
4
x := notemap["WAIT"]
// = 0
y := notemap["STOP_HAMMERTIME"]
// y = 0

If you’re reading this, there’s a pretty good chance you know this. But I wanted to highlight it, as I think this is a pretty cool feature of Go! Yet, in this case, I chose to be explicit about the intend of the WAIT note.

Playing a note

Alright, so we have our notes and our frequencies associated with them. We still need to actually generate a wave. For this I’ve introduced the convenience function “play” which takes a few parameters and returns a frame:

play(LookupOscillator, Note, Duration, Notefreqs) -> []Frame

This is quite similar to the past about ADSR, and just as in that post we will apply the ADSR envelope to the signal here as well. Notice that ‘play’ takes a LookupOscillator. In the previous post we’ve build a few of these for generating sine waves, triangle, square, etc.

This function tells us, play a note for a certain duration (in seconds) with a given oscillator, and return a slice of frames. This function allowed me to quickly change the type of wave, and play around with the result of playing different notes for different timespans.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func play(l *synth.LookupOscillator, note string, duration float64, notefreqs map[string]float64) []wave.Frame {
	freq := notefreqs[note]

	frames := make([]wave.Frame, int(duration*sr))
	var adsrtime int
	for i := range frames {
		value := synth.ADSR(0.8, duration, 0.2, 0.1, 0.5, duration-0.05, sr, adsrtime)
		adsrtime++
		frames[i] = wave.Frame(value * l.InterpolateTick(freq))
	}

	return frames
}

Getting the ADSR envelope to sound like I wanted to was honestly the goold old way of trial and error. 😅.

Composing the song

Finally we have everything ready to compose the song. The below code is definitely not pretty, but it gets the job done! First we generate the notes, then we create a wave table of a shape that we’d like. We plug this into an Oscillator, and then we start calling the Play function with the notes that we need.

For convenience I’ve added some variables to easily adapt the duration of sections of the song. Variables, yeah!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func main() {
	nm := generateNotes()
	st, err := synth.NewTriangleTable(8*1024, 11)
	if err != nil {
		panic(err)
	}
	osc, err := synth.NewLookupOscillator(sr, st, 0)
	if err != nil {
		panic(err)
	}

	waitdur := .1
	cdecdur := 0.6
	efgdur := 0.6
	gagfecdur := 0.5
	cgcdur := 0.5

	// notes: https://www.true-piano-lessons.com/frere-jacques.html
	output := []wave.Frame{}
	// frere jacues
	output = append(output, play(osc, "C5", cdecdur, nm)...)
	output = append(output, play(osc, "D5", cdecdur, nm)...)
	output = append(output, play(osc, "E5", cdecdur, nm)...)
	output = append(output, play(osc, "C5", cdecdur, nm)...)
	output = append(output, play(osc, "C5", cdecdur, nm)...)
	output = append(output, play(osc, "D5", cdecdur, nm)...)
	output = append(output, play(osc, "E5", cdecdur, nm)...)
	output = append(output, play(osc, "C5", cdecdur, nm)...)
	output = append(output, play(osc, "WAIT", waitdur, nm)...)
	// dormez-vous
	output = append(output, play(osc, "E5", efgdur, nm)...)
	output = append(output, play(osc, "F5", efgdur, nm)...)
	output = append(output, play(osc, "G5", efgdur, nm)...)
	output = append(output, play(osc, "WAIT", 0.2, nm)...)
	output = append(output, play(osc, "E5", efgdur, nm)...)
	output = append(output, play(osc, "F5", efgdur, nm)...)
	output = append(output, play(osc, "G5", efgdur, nm)...)
	// sonnez les matines
	output = append(output, play(osc, "G5", gagfecdur, nm)...)
	output = append(output, play(osc, "A5", gagfecdur, nm)...)
	output = append(output, play(osc, "G5", gagfecdur, nm)...)
	output = append(output, play(osc, "F5", gagfecdur, nm)...)
	output = append(output, play(osc, "E5", gagfecdur, nm)...)
	output = append(output, play(osc, "C5", gagfecdur, nm)...)
	output = append(output, play(osc, "WAIT", 0.2, nm)...)
	output = append(output, play(osc, "G5", gagfecdur, nm)...)
	output = append(output, play(osc, "A5", gagfecdur, nm)...)
	output = append(output, play(osc, "G5", gagfecdur, nm)...)
	output = append(output, play(osc, "F5", gagfecdur, nm)...)
	output = append(output, play(osc, "E5", gagfecdur, nm)...)
	output = append(output, play(osc, "C5", gagfecdur, nm)...)
	output = append(output, play(osc, "WAIT", gagfecdur, nm)...)
	// dindindon
	output = append(output, play(osc, "C5", cgcdur, nm)...)
	output = append(output, play(osc, "G5", cgcdur, nm)...)
	output = append(output, play(osc, "C5", cgcdur, nm)...)
	output = append(output, play(osc, "WAIT", 0.1, nm)...)
	output = append(output, play(osc, "C5", cgcdur, nm)...)
	output = append(output, play(osc, "G5", cgcdur, nm)...)
	output = append(output, play(osc, "C5", cgcdur, nm)...)

	wfmt := wave.NewWaveFmt(1, 1, sr, 16, nil)
	wave.WriteFrames(output, wfmt, "frerejacques.wav")
}

Download the result, or view the video at the beginning of this post.

Or, viewed in audacity


If you liked this and want to know when I write new posts, the best way to keep up to date is by following me on twitter.