7

Let's Make Overtone Sing Like Sonic-Pi

 2 years ago
source link: https://savo.rocks/posts/lets-make-overtone-sing-like-sonic-pi/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
4 hours ago2022-05-09T11:35:00+02:00 by Savo Djuric

Introduction

In my Previous Articles I have described how to setup your environment for Overtone regarding system audio, basic Overtone project, VS Code IDE and Emacs editor/IDE. This and the following articles will be completely environment agnostic, meaning you can follow it regardless of what is your preferred setup, as long as it satisfied general requirements.

Overtone Playground

If you want everything to just work, it’s best to clone Overtone Playground project from GitHub and start the REPL in core namespace. This project and the guide for it is sponsored by Clojurists Together in Q1 of 2022 batch. The purpose is to create a guide that will be effortless to users, inspired by Sonic-Pi Tutorial which is very straight-forward and optimized to get you experimenting with music right away. If you also want to try Sonic-Pi follow the link to project’s web page for further instructions.

Simplest Sounds

Although Sonic-Pi tutorials’ first chapter shows an interesting and powerful concept of live_loop I decided to skip it for now and cover it later since it isn’t easy to implement and there are simpler concepts to live coding, such as the play function, which is described in the second chapter of the tutorial. I will, however, introduce you to a simple looper function in the next article.

So, the first line of code of code in Sonic-Pi’s tutorial second chapter is play 70. It tells our program to play the certain note. In Overtone Playground we have the similar function an we can call it like this: (play 70). It looks almost the same, except for the set of parenthesis at the beginning and the end. You will see those a lot in my tutorials. You should hear a single sound. Try executing it as many times as you want, even in some kind of rhythm if you want to play that way. When you get tired of that try changing the number, as the Sonic-Pi tutorial suggests: (play 75) or (play 60) (notice that this two code samples I gave you are with parenthesis, you can copy/paste them in your REPL, they will work). You can see the details in the Sonic-Pi tutorial, but in short: low numbers produce lower-pitched sounds and high numbers produce higher-pitched sounds. The play function uses MIDI note numbering to produce notes, so (play 70) produces A#4 note, (play 75) will give us D#5, and (play 60) results in C4, or “middle C”, as it is popularly known. If you want to see all the midi notes, their traditional names (in English and German) and corresponding frequency, take a look at this page. You can also bookmark it, since it is pretty useful to keep open when you want to combine various notes to sound good.

Chords

Moving on in the tutorial, we see that in Sonic-Pi we can play multiple notes at the same time, with these three lines:

play 72
play 75
play 79

The same effect can be achieved in Overtone-Playground with this code:

(play 72 75 79)

Do you notice something? While in Sonic-Pi we had to call play function for every note, in Overtone-Playground we just had to do it once. That is because I used power of Clojure to create a play function that accepts multiple arguments (although it wasn’t necessary, as Clojure already supports this, and I’ll explain how in a bit). Let’s take a closer look at the function.

Play Function Under The Microscope

(defn play
  "Can be used in following ways:

  For playing single notes:
  (play 60)
  (play :C4)

  For playing multiple notes/chords (accepts both collections and multiple-single arguments):
  (play 57 60 64)
  (play [57 60 64])
  (play :c3 :a4 :f3) <-- arguments are case insensitive, btw.
  (play [:C3 :A4 :F3]) <-- as you can see here.
  (play (chord :a3 :minor))"

  ([]
   (play 60))
  ([x]
   (if (seqable? x)
       (map play x)
       (if (keyword? x)
         (sth/overpad (note x))
         (sth/overpad x))))
  ([x & args]
   (play (conj args x))))

First line defines a new function and names it play. Lines 2 to 14 are function description which you can access anytime directly in the REPL by typing (doc play). As you can see, I described in which ways you can use the play function, and there are several ways to do this, as this function can accept multiple arguments. You can learn more about this powerful concept here.
We can call play in three ways: with no arguments, just one argument, or multiple arguments.

  • Calling the function with no argument:
(play)

As you can see this is very simple and easy. The function is written so that if no arguments are given it will default to (play 60), as can be seen in this part of the function:

([]
   (play 60))

The above is just an excerpt from the whole function so it is clearer which part of the function is executed. Moving on,

  • Calling the function with one argument:
(play 70)

This is also very simple for the user, but this function call gets executed by the core part of the function which is:

([x]
   (if (seqable? x)
       (map play x)
       (if (keyword? x)
         (sth/overpad (note x))
         (sth/overpad x))))

Let’s look at this part of the function line by line:

  1. tells us that this part of the function is executed when we give it one argument.
  2. checks if the argument we supplied is seqable. In short, it checks if argument is a collection of numbers or keywords (more about those soon), for example a vector or just a single number or keyword (hint: just for fun try calling play function with a string like (play "yo") and see what you’ll get.
  3. (map play x) gets executed if we indeed supplied a collection as an argument to the play function. This is where the power of Clojure is utilized very well. it uses the map function to execute play function with every argument in the collection. Think of it this way: calling (map play [60 62 65]) is the same as executing the following code at the same time:
    (play 60)
    (play 62)
    (play 65)
    It is, in fact, little more complex than that under the hood, but let’s accept this simple explanation for now. If you want to learn more about the powerful map function, visit this link.
  4. Next line gets executed if we called play function with one number or keyword as an argument, like (play :C4). Yes, this is what a keyword looks like. it begins with a colon : and should NOT be followed by a number (see the link on keywords for more info). In Overtone, we can use keywords to represent notes that are easier for people to read. In this example, :C4 corresponds to a C note in 4th octave.
  5. (sth/overpad (note x)) gets executed if we supplied the keyword as an argument to the play function. It uses overpad synth that we imported from overtone.inst.synth namespace (if you don’t understand this terminology don’t worry, as it is not easy to grasp everything, especially if you haven’t seen none of this before. We are just using a certain synthesizer to play our note). The note function takes our :C4 keyword and returns a equivalent MIDI note number that gets passed as an argument to our synth.
  6. If the supplied note isn’t a keyword, but number, the (sth/overpad x) part of code will get evaluated, passing the number to our overpad synth.

Next up we have:

  • Calling the function with multiple (two or more) arguments:
(play 72 76 79)
;; or
(play :c5 :d#5 :g5)

Two calls to the play function above are actually the same, since given keywords correspond to the given MIDI notes. In this case, the following part of play function gets executed:

([x & args]
   (play (conj args x)))

[x & args] is Clojure syntax for variadic functions in which the first argument is x, and other arguments (you can supply as many as you want) get collected in a sequence. But we don’t want that. We want all our arguments to be in the same collection so it all gets executed. In the next line, (conj args x) puts the first argument in collection with others. Then (play... is called inside itself (another awesome feature of Clojure), but we are now giving it just one argument that is a seqable collection, so it will execute the code that was thoroughly described in the calling the function with one argument section above.
One more thing: Maybe you know what chords go well one after another, but you don’t know or don’t want to call play with individual notes? This is not a problem, as Overtone has a chord function where you supply the root note and chord type as keywords, and the function returns MIDI notes of that particular chord. Try evaluating (chord :c4 :minor), or (chord :a3 :m11) and you’ll get a collection of MIDI notes that you can pass to play. Try (play (chord :c4 :minor)) and (play (chord :a3 :m11)) and listen to what it produces. Those are some interesting sounds.

Wrapping it up

I wanted to cover more things in this article, but it is getting a bit long now and I don’t want it to be overwhelming. We took a detailed look at some basic Overtone functionalities, made easy with Overtone-Playground and explored some of the powers that Clojure as a language offers us. In the next article we will continue with Sonic-Pi tutorial and cover the concept of melody and we will play a little with looping the sounds. Please feel free to ask questions if something is too hard to understand, or if you have suggestions or comments about the article.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK