-
Notifications
You must be signed in to change notification settings - Fork 117
Your First Synth
This tutorial follows on from part 1 of the user guide, and will probably make more sense if you have worked through that first.
In part 1 you saw that you could generate continuous tones by creating and connecting nodes together. This technique works fine for examples playing single notes and is very simple to get started with. When you are looking to play melodies and rhythms, however, it quickly becomes time-consuming to keep creating and connecting nodes.
In order to simplify the process of creating groups of nodes, we can introduces the idea of a group. Groups are basically classes which contain the instructions for how to create and connect networks of nodes. Synths are therefore groups which generate audio. When a synth has been defined, instead of creating each node needed to generate a note, you can create an instance of the synth and connect it to the audio output.
To define our first synth we can use most of the code we wrote in part 1. Copy the AudioletApp class and paste it within the domready function, then rename the class to Synth. To turn the Synth class into a group we first need to make it a subclass of AudioletGroup.
...
var Synth = function(audiolet, frequency) {
AudioletGroup.apply(this, [audiolet, 0, 1]);
...
};
extend(Synth, AudioletGroup);
...
The last line here tells the Synth class to extend the AudioletGroup class, and the second line calls the AudioletGroup's constructor function. You can see that rather than creating the Audiolet class as part of the synth we instead pass it as an argument to the constructor. This is similar to how we pass the Audiolet context when we are creating new nodes; externally the AudioletGroup class behaves in exactly the same way as the AudioletNode class.
The AudioletGroup constructor takes two further argument, defining the number of inputs and outputs it will use. As our group will create audio rather than process it we do not need any inputs and need only a single output. Finally, you will notice that we have added a frequency argument, which will allow us to create notes of any frequency using the same synth.
The next step is to make some slight modifications to the node generation and connection code in order to make it frequency-independent and able to make connections via the AudioletGroup's outputs.
...
AudioletGroup.apply(this, [audiolet, 0, 1]);
this.sine = new Sine(this.audiolet, frequency);
this.modulator = new Saw(this.audiolet, 2 * frequency);
this.modulatorMulAdd = new MulAdd(this.audiolet, frequency / 2,
frequency);
this.modulator.connect(this.modulatorMulAdd);
this.modulatorMulAdd.connect(this.sine);
this.sine.connect(this.outputs[0]);
};
extend(Synth, AudioletGroup);
...
The main change here is to convert any specific frequencies to multiples of the input frequency. The only other change is that instead of connecting the sine oscillator to the audiolet output we instead connect to the first (and only) output of our AudioletGroup.
Now that we have converted our basic code to a synth we can make the synth play by making a new version of the AudioletApp class.
...
var AudioletApp = function() {
this.audiolet = new Audiolet();
var synth = new Synth(this.audiolet, 440);
synth.connect(this.audiolet.output);
};
this.audioletApp = new AudioletApp();
};
You can see that now it is very simple to create new instances of the synth and connect them up without having to write out all of the nodes and connections involved. If you load up the page in your browser you should hear the modulated tone which we had at the end of the previous example.
That's pretty much it for turning our existing code into a synth. Before we finish though, there's one more small change which we can make to set us up for playing melodies in the next part of the guide: adding an amplitude envelope to the synth. So far we have only looked at creating continuous tones, but by adding envelope we will be able to play discrete notes.
The first stage of adding an amplitude envelope is to add a gain node to the end of our chain of nodes, which can be used to control how loud the output is. The synth should look something like:
...
var Synth = function(audiolet, frequency) {
AudioletGroup.apply(this, [audiolet, 0, 1]);
this.sine = new Sine(this.audiolet, frequency);
this.modulator = new Saw(this.audiolet, frequency * 2);
this.modulatorMulAdd = new MulAdd(this.audiolet, frequency / 2,
frequency);
this.gain = new Gain(this.audiolet);
this.modulator.connect(this.modulatorMulAdd);
this.modulatorMulAdd.connect(this.sine);
this.sine.connect(this.gain);
this.gain.connect(this.outputs[0]);
};
extend(Synth, AudioletGroup);
...
We can then add the envelope and use it to change the amount of gain over time, by connecting its output to the second input of the Gain node.
...
this.gain = new Gain(this.audiolet);
this.envelope = new PercussiveEnvelope(this.audiolet, 1, 0.2, 0.5,
function() {
this.audiolet.scheduler.addRelative(0,
this.remove.bind(this));
}.bind(this)
);
this.modulator.connect(this.modulatorMulAdd);
this.modulatorMulAdd.connect(this.sine);
this.envelope.connect(this.gain, 0, 1);
this.sine.connect(this.gain);
this.gain.connect(this.outputs[0]);
...
This introduces a number of concepts which haven't been covered yet. First of all, point your browser at the index page and have a listen. You should hear the tone from earlier, but played as a single short note. Now we can have a closer look at the code. The third and fourth arguments of the PercussiveEnvelope constructor control the envelope's attack and release; a PercussiveEnvelope is very simple and consists of a linear attack section immediately followed by a linear release section. The second argument to the envelope is the gate control, which can either be 1 or 0. As we want the note to play straight away, we can set this to 1 and the envelope will be triggered instantly.
The final argument to the envelope is a little more complicated, and allows us to pass a function to be called when the envelope has finished its release phase. In this example the function calls the AudioletGroup.remove function, which disconnects the synth from the processing graph, meaning we don't waste CPU cycles generating silence. The remove function isn't called straight away, but is instead run using the Audiolet scheduler. By scheduling the removal we ensure that the synth is only removed when it is safe to do so.
Finally, with the envelope created, we can connect it up to the Gain node. Here we use the more explicit version of the AudioletNode.connect function, where we supply the input and output indices to connect together. This is because we need to connect the envelope to the second input of the Gain node, which controls the gain level (with the first input receiving the audio input from the sine oscillator).