A Gwion 'Hello, World'
Let's get started using gwion!
A simple example, step by step
We are going to write a typical Gwion Hello, World!
Prepare to make sound
In Gwion, a program typically makes sound.
Request needed plugin
Now we create a Sinusoidal generator
and connect it to thee audio output
(dac
stands for Digital to Audio Converter)
using the ~>
operator.
#import Modules
new SinOsc ~> dac;
the
SinOsc
class comes from theModules
plugin
Greet the world
Indeed, a Hello, world has to print something.
We do so using the gack operator (<<<
... >>>
)
<<< "Hello, World!" >>>;
Let time pass
Let's face it, we did not make any sound yet: No time passed, and if no time passed, there can't have been any sound. We can wait some time using
5::second => now;
Try it
Here is the complete example, editable and runnable.
Have fun!
#import Modules
<<< "Hello, World!" >>>;
new SinOsc ~> dac;
5::second => now;
Declaring a Variable
In the former example, we just created a sinusoidal oscillator, without a way to access it later.
This time, let's declare the oscillator [pronouns are also good to avoid] so it has a usable name, and change some things like volume and frequency
#import Modules
<<< "Hello, World!" >>>;
#! create a Sinusoidal oscillator, named sine
#! and connect it to the speakers
var SinOsc sine ~> dac;
#! let some time pass
1::second => now;
#! change the frequency
220 => sine.freq;
#! let some time pass
1::second => now;
#! change the volume
0.1 => sine.amp;
.5::second => now;
#! change the volume, again
0.6 => sine.amp;
.5::second => now;
#! change the frequency
440 => sine.freq;
#! let some time pass
2::second => now;
Using Functions
The former example raises a question, will we always need to type
some_freq => sine.freq;
some_duration => now;
everytime we want to set the frequency and advance time?
Functions as mean to encapsulate behavior
Well, no: this can all fit in a function. Let's write it
fun void play(SinOsc sine, const float freq, const dur d) {
freq => sine.freq;
d => now;
}
What does this mean?
the fun
keyword denotes the start of a function declaration.
the return type of the function follows.
here the function returns nothing, we declare it as void
.
After that there is the function name, play
in this case.
What follows is the argument list.
A function argument syntax is as follow:
Type name
A simple example function
Let's write a simple adder
fun int add(int a, int b) {
return a + b;
}
This time the function returns an int, so we change the return type accordingly.
Here is how we call it
fun int add(int a, int b) {
return a + b;
}
<<< add(1, 2) >>>;
#! alternatively
<<< (2, 3) => add >>>;
A example using functions
#import Modules
fun void play(SinOsc sine, const float freq, const dur d) {
freq => sine.freq;
d => now;
}
var SinOsc sine ~> dac;
play(sine, 440, second);
play(sine, 220, .5::second);
play(sine, 220, .5::second);
play(sine, 440, second);
A little psycho-acoustic experiment
Let's now try something that will put in evidence that the distinction between melody and noise is only perceptual.
Look at the snippet (and listen the result)
#import Math
#import Modules
#define NCYCLES 2
1 :=> var int compensate;
second :=> var dur d;
const SinOsc s ~> dac;
while(d >= samp) {
repeat(NCYCLES * compensate) {
Math.rand2f(220, 880) => s.freq;
d => now;
}
d / 2. :=> d;
2 *=> compensate;
}
Here we loop until our base duration is one sample
Until that we play NCYCLES * compensate
notes of our duration.
Notes frequencies are randomly chosen between a low and a high bound
(here 220
and 880
, aka A3 and A5).
At first we hear some boring slow melody.
It then gets faster and faster,
up to the point we only hear noise.
But if you listen carefully,
you'll hear that those noises have different granularities.
Near the very end, it's almost like if the noise is whisteling,
and the thing is that the frequency at which it whistles
is the median between the low and the bounds.
Note: line 4, we use a preprocessor
define
to avoid magic numbers in our code
(this helps with configurability too)
Basic Argument handling
Let's look at this snippet
#import Std
#import Math
#import Modules
fun int get_ncycles() {
if(me.args())
return me.arg(0) => Std.atoi;
return 2;
}
fun float get_user_duration() {
if(me.args() > 1)
return me.arg(1) => Std.atof;
return 1;
}
fun dur get_duration() {
return get_user_duration()::second;
}
get_ncycles() :=> const int ncycles;
get_duration() :=> var dur duration;
1 :=> var int compensate;
const SinOsc s ~> dac;
while(duration >= samp) {
repeat(ncycles * compensate) {
Math.rand2f(220, 880) :=> s.freq;
duration => now;
}
duration/2. :=> duration;
2 *=> compensate;
}
Yes, it's almost the same as the previous one, but this one provides argument handling.
For instance this function:
fun int get_ncycles() {
if(me.args())
return me.arg(0) => Std.atoi;
return 2;
}
checks if the file was called with any arguments, using me.args()
.
If there is at least one argument, we get it as a string, using me.arg(0)
since we want the first argument,
then we get the int
value out of it, with Std.atoi
.
If there is no argument, it defaults to 2
.